/*-------------------------------------------------------------------------
 *
 * planner.c
 *	  查询优化器外部接口。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/plan/planner.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>
#include <math.h>

#include "access/genam.h"
#include "access/htup_details.h"
#include "access/parallel.h"
#include "access/sysattr.h"
#include "access/table.h"
#include "access/xact.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "executor/nodeAgg.h"
#include "foreign/fdwapi.h"
#include "jit/jit.h"
#include "lib/bipartite_match.h"
#include "lib/knapsack.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
#include "optimizer/appendinfo.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
#include "optimizer/paramassign.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/plancat.h"
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
#include "parser/parse_agg.h"
#include "parser/parsetree.h"
#include "partitioning/partdesc.h"
#include "rewrite/rewriteManip.h"
#include "storage/dsm_impl.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/selfuncs.h"
#include "utils/syscache.h"

/* GUC 参数 */
double		cursor_tuple_fraction = DEFAULT_CURSOR_TUPLE_FRACTION;
int			force_parallel_mode = FORCE_PARALLEL_OFF;
bool		parallel_leader_participation = true;

/* 插件在 planner() 中获取控制的钩子 */  
planner_hook_type planner_hook = NULL;

/* 插件在 grouping_planner() 规划上层关系时获取控制的钩子 */  
create_upper_paths_hook_type create_upper_paths_hook = NULL;


/* preprocess_expression 的表达式类型代码 */  
#define EXPRKIND_QUAL				0
#define EXPRKIND_TARGET				1
#define EXPRKIND_RTFUNC				2
#define EXPRKIND_RTFUNC_LATERAL		3
#define EXPRKIND_VALUES				4
#define EXPRKIND_VALUES_LATERAL		5
#define EXPRKIND_LIMIT				6
#define EXPRKIND_APPINFO			7
#define EXPRKIND_PHV				8
#define EXPRKIND_TABLESAMPLE		9
#define EXPRKIND_ARBITER_ELEM		10
#define EXPRKIND_TABLEFUNC			11
#define EXPRKIND_TABLEFUNC_LATERAL	12

/* standard_qp_callback 的传递数据 */  
typedef struct
{
	List	   *activeWindows;	/* 活动窗口（如果有的话） */  
	List	   *groupClause;	/* 重写 parse->groupClause */  
} standard_qp_extra;

/*  
 * 特定于分组集的数据  
 */  

typedef struct
{
	List	   *rollups;
	List	   *hash_sets_idx;
	double		dNumHashGroups;
	bool		any_hashable;
	Bitmapset  *unsortable_refs;
	Bitmapset  *unhashable_refs;
	List	   *unsortable_sets;
	int		   *tleref_to_colnum_map;
} grouping_sets_data;

/*  
 * 在 WindowClause 重新排序过程中用于临时结构，以便能够  
 * 根据分区/排序前缀对 WindowClauses 进行排序。  
 */  
typedef struct
{
	WindowClause *wc;
	List	   *uniqueOrder;	/* 唯一的排序/分区子句列表 per Window */
} WindowClauseSortData;

/* 本地函数 */
static Node *fc_preprocess_expression(PlannerInfo *fc_root, Node *fc_expr, int fc_kind);
static void fc_preprocess_qual_conditions(PlannerInfo *fc_root, Node *fc_jtnode);
static void fc_grouping_planner(PlannerInfo *fc_root, double fc_tuple_fraction);
static grouping_sets_data *fc_preprocess_grouping_sets(PlannerInfo *fc_root);
static List *fc_remap_to_groupclause_idx(List *fc_groupClause, List *fc_gsets,
									  int *fc_tleref_to_colnum_map);
static void fc_preprocess_rowmarks(PlannerInfo *fc_root);
static double fc_preprocess_limit(PlannerInfo *fc_root,
							   double fc_tuple_fraction,
							   int64 *fc_offset_est, int64 *fc_count_est);
static void fc_remove_useless_groupby_columns(PlannerInfo *fc_root);
static List *fc_preprocess_groupclause(PlannerInfo *fc_root, List *fc_force);
static List *fc_extract_rollup_sets(List *fc_groupingSets);
static List *fc_reorder_grouping_sets(List *fc_groupingSets, List *fc_sortclause);
static void fc_standard_qp_callback(PlannerInfo *fc_root, void *fc_extra);
static double get_number_of_groups(PlannerInfo *fc_root,
								   double fc_path_rows,
								   grouping_sets_data *fc_gd,
								   List *fc_target_list);
static RelOptInfo *fc_create_grouping_paths(PlannerInfo *fc_root,
										 RelOptInfo *fc_input_rel,
										 PathTarget *fc_target,
										 bool fc_target_parallel_safe,
										 grouping_sets_data *fc_gd);
static bool fc_is_degenerate_grouping(PlannerInfo *fc_root);
static void fc_create_degenerate_grouping_paths(PlannerInfo *fc_root,
											 RelOptInfo *fc_input_rel,
											 RelOptInfo *fc_grouped_rel);
static RelOptInfo *fc_make_grouping_rel(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
									 PathTarget *fc_target, bool fc_target_parallel_safe,
									 Node *fc_havingQual);
static void fc_create_ordinary_grouping_paths(PlannerInfo *fc_root,
										   RelOptInfo *fc_input_rel,
										   RelOptInfo *fc_grouped_rel,
										   const AggClauseCosts *fc_agg_costs,
										   grouping_sets_data *fc_gd,
										   GroupPathExtraData *fc_extra,
										   RelOptInfo **fc_partially_grouped_rel_p);
static void fc_consider_groupingsets_paths(PlannerInfo *fc_root,
										RelOptInfo *fc_grouped_rel,
										Path *fc_path,
										bool fc_is_sorted,
										bool fc_can_hash,
										grouping_sets_data *fc_gd,
										const AggClauseCosts *fc_agg_costs,
										double fc_dNumGroups);
static RelOptInfo *fc_create_window_paths(PlannerInfo *fc_root,
									   RelOptInfo *fc_input_rel,
									   PathTarget *fc_input_target,
									   PathTarget *fc_output_target,
									   bool fc_output_target_parallel_safe,
									   WindowFuncLists *fc_wflists,
									   List *fc_activeWindows);
static void fc_create_one_window_path(PlannerInfo *fc_root,
								   RelOptInfo *fc_window_rel,
								   Path *fc_path,
								   PathTarget *fc_input_target,
								   PathTarget *fc_output_target,
								   WindowFuncLists *fc_wflists,
								   List *fc_activeWindows);
static RelOptInfo *fc_create_distinct_paths(PlannerInfo *fc_root,
										 RelOptInfo *fc_input_rel);
static void fc_create_partial_distinct_paths(PlannerInfo *fc_root,
										  RelOptInfo *fc_input_rel,
										  RelOptInfo *fc_final_distinct_rel);
static RelOptInfo *fc_create_final_distinct_paths(PlannerInfo *fc_root,
											   RelOptInfo *fc_input_rel,
											   RelOptInfo *fc_distinct_rel);
static RelOptInfo *fc_create_ordered_paths(PlannerInfo *fc_root,
										RelOptInfo *fc_input_rel,
										PathTarget *fc_target,
										bool fc_target_parallel_safe,
										double fc_limit_tuples);
static PathTarget *fc_make_group_input_target(PlannerInfo *fc_root,
										   PathTarget *fc_final_target);
static PathTarget *fc_make_partial_grouping_target(PlannerInfo *fc_root,
												PathTarget *fc_grouping_target,
												Node *fc_havingQual);
static List *fc_postprocess_setop_tlist(List *fc_new_tlist, List *fc_orig_tlist);
static List *fc_select_active_windows(PlannerInfo *fc_root, WindowFuncLists *fc_wflists);
static PathTarget *fc_make_window_input_target(PlannerInfo *fc_root,
											PathTarget *fc_final_target,
											List *fc_activeWindows);
static List *fc_make_pathkeys_for_window(PlannerInfo *fc_root, WindowClause *fc_wc,
									  List *fc_tlist);
static PathTarget *fc_make_sort_input_target(PlannerInfo *fc_root,
										  PathTarget *fc_final_target,
										  bool *fc_have_postponed_srfs);
static void fc_adjust_paths_for_srfs(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								  List *fc_targets, List *fc_targets_contain_srfs);
static void add_paths_to_grouping_rel(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
									  RelOptInfo *fc_grouped_rel,
									  RelOptInfo *fc_partially_grouped_rel,
									  const AggClauseCosts *fc_agg_costs,
									  grouping_sets_data *fc_gd,
									  double fc_dNumGroups,
									  GroupPathExtraData *fc_extra);
static RelOptInfo *fc_create_partial_grouping_paths(PlannerInfo *fc_root,
												 RelOptInfo *fc_grouped_rel,
												 RelOptInfo *fc_input_rel,
												 grouping_sets_data *fc_gd,
												 GroupPathExtraData *fc_extra,
												 bool fc_force_rel_creation);
static void fc_gather_grouping_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static bool fc_can_partial_agg(PlannerInfo *fc_root);
static void fc_apply_scanjoin_target_to_paths(PlannerInfo *fc_root,
										   RelOptInfo *fc_rel,
										   List *fc_scanjoin_targets,
										   List *fc_scanjoin_targets_contain_srfs,
										   bool fc_scanjoin_target_parallel_safe,
										   bool fc_tlist_same_exprs);
static void fc_create_partitionwise_grouping_paths(PlannerInfo *fc_root,
												RelOptInfo *fc_input_rel,
												RelOptInfo *fc_grouped_rel,
												RelOptInfo *fc_partially_grouped_rel,
												const AggClauseCosts *fc_agg_costs,
												grouping_sets_data *fc_gd,
												PartitionwiseAggregateType fc_patype,
												GroupPathExtraData *fc_extra);
static bool fc_group_by_has_partkey(RelOptInfo *fc_input_rel,
								 List *fc_targetList,
								 List *fc_groupClause);
static int	fc_common_prefix_cmp(const void *fc_a, const void *fc_b);


/*****************************************************************************
 *
 *	   查询优化器入口点
 *
 * 为了支持可加载的插件以监控或修改规划器行为，我们提供了一个钩子变量，允许插件在标准规划过程之前和之后控制。插件通常会调用
 * standard_planner()。
 *
 * 对于插件作者的说明：standard_planner() 会修改其 Query 输入，因此如果你希望多次规划，最好复制该数据结构。
 *
 *****************************************************************************/
PlannedStmt * planner(Query *fc_parse, const char *fc_query_string, int fc_cursorOptions,
		ParamListInfo fc_boundParams)
{
	PlannedStmt *fc_result;

	if (planner_hook)
		fc_result = (*planner_hook) (fc_parse, fc_query_string, fc_cursorOptions, fc_boundParams);
	else
		fc_result = standard_planner(fc_parse, fc_query_string, fc_cursorOptions, fc_boundParams);
	return fc_result;
}

PlannedStmt * standard_planner(Query *fc_parse, const char *fc_query_string, int fc_cursorOptions,
				 ParamListInfo fc_boundParams)
{
	PlannedStmt *fc_result;
	PlannerGlobal *fc_glob;
	double		fc_tuple_fraction;
	PlannerInfo *fc_root;
	RelOptInfo *fc_final_rel;
	Path	   *fc_best_path;
	Plan	   *fc_top_plan;
	ListCell   *fc_lp,
			   *fc_lr;

	/*
	 * 为此次规划器调用设置全局状态。此数据在给定命令中可能存在的所有子查询级别中都是需要的，
	 * 因此我们将其保存在一个单独的结构中，该结构由每个 per-Query PlannerInfo 链接。
	 */
	fc_glob = makeNode(PlannerGlobal);

	fc_glob->boundParams = fc_boundParams;
	fc_glob->subplans = NIL;
	fc_glob->subroots = NIL;
	fc_glob->rewindPlanIDs = NULL;
	fc_glob->finalrtable = NIL;
	fc_glob->finalrowmarks = NIL;
	fc_glob->resultRelations = NIL;
	fc_glob->appendRelations = NIL;
	fc_glob->relationOids = NIL;
	fc_glob->invalItems = NIL;
	fc_glob->paramExecTypes = NIL;
	fc_glob->lastPHId = 0;
	fc_glob->lastRowMarkId = 0;
	fc_glob->lastPlanNodeId = 0;
	fc_glob->transientPlan = false;
	fc_glob->dependsOnRole = false;

	/*
	 * 评估是否可以为此查询使用并行模式。我们不能在独立后端中执行此操作，或者如果命令尝试修改任何数据，或者如果这是一个游标操作，
	 * 或者如果 GUC 被设置为不允许并行的值，或者如果查询树中存在并行不安全的函数。
	 *
	 * （注意，我们确实允许 CREATE TABLE AS、SELECT INTO 和 CREATE MATERIALIZED VIEW 使用并行计划，
	 * 但这只有在命令写入一个全新表时才是安全的，工作进程将无法看到该表。如果工作进程可以看到该表，
	 * 那么组锁会导致它们忽略领导者的重型 GIN 页面锁，这使得这是不安全的。如果我们想要允许一般的并行插入，
	 * 那么我们必须以某种方式修复这个问题；更新和删除在组合 CID 周围有额外的问题。）
	 *
	 * 目前，如果我们在并行工作进程内部运行，则不尝试使用并行模式。最终我们可能能够放宽此限制，但目前似乎最好
	 * 是不让并行工作进程尝试创建自己的并行工作进程。
	 */
	if ((fc_cursorOptions & CURSOR_OPT_PARALLEL_OK) != 0 &&
		IsUnderPostmaster &&
		fc_parse->commandType == CMD_SELECT &&
		!fc_parse->hasModifyingCTE &&
		max_parallel_workers_per_gather > 0 &&
		!IsParallelWorker())
	{
		/* 所有简单的测试通过了，因此扫描查询树 */
		fc_glob->maxParallelHazard = max_parallel_hazard(fc_parse);
		fc_glob->parallelModeOK = (fc_glob->maxParallelHazard != PROPARALLEL_UNSAFE);
	}
	else
	{
		/* 跳过查询树扫描，假设它不安全 */
		fc_glob->maxParallelHazard = PROPARALLEL_UNSAFE;
		fc_glob->parallelModeOK = false;
	}

	/*
	 * glob->parallelModeNeeded 通常在这里设置为 false，并在实际创建 Gather 或 Gather Merge 计划时更改为 true
	 * （参见 create_gather_plan, create_gather_merge_plan）。
	 *
	 * 但是，如果 force_parallel_mode = on 或 force_parallel_mode = regress，
	 * 那么只要安全，我们就强加并行模式，即使最终计划不使用并行性。
	 * 如果查询包含任何并行不安全的内容，则不安全；在这种情况下 parallelModeOK 会为 false。
	 * 请注意，此时 parallelModeOK 不能更改。
	 * 否则，查询中的所有内容都要么是并行安全的，要么是并行受限的，在这两种情况下都应该可以强加并行模式限制。
	 * 如果最终导致某些内容损坏，那么用户在查询中包含的某些函数被错误地标记为并行安全或并行受限，而实际上它是并行不安全的，
	 * 或者查询规划器本身存在缺陷。
	 */
	fc_glob->parallelModeNeeded = fc_glob->parallelModeOK &&
		(force_parallel_mode != FORCE_PARALLEL_OFF);

	/* 确定计划中可能被扫描的比例 */
	if (fc_cursorOptions & CURSOR_OPT_FAST_PLAN)
	{
		/*
		 * 我们并不真正知道用户最终会从游标中 FETCH 多少元组，但通常情况下他并不想要“所有”，
		 * 或者希望快速启动计划，这样他就可以更早处理一部分元组。使用 GUC 参数来决定优化什么比例。
		 */
		fc_tuple_fraction = cursor_tuple_fraction;

		/*
		 * 我们将 cursor_tuple_fraction 记录为简单的比例，这意味着边缘情况 0 和 1 必须在这里特别处理。
		 * 我们将 1 转换为 0（“所有元组”）并将 0 转换为非常小的比例。
		 */
		if (fc_tuple_fraction >= 1.0)
			fc_tuple_fraction = 0.0;
		else if (fc_tuple_fraction <= 0.0)
			fc_tuple_fraction = 1e-10;
	}
	else
	{
		/* 默认假设是我们需要所有的元组 */
		fc_tuple_fraction = 0.0;
	}

	/* 主规划入口点（可能为子查询递归） */
	fc_root = subquery_planner(fc_glob, fc_parse, NULL,
							false, fc_tuple_fraction);

	/* 选择最佳路径并将其转换为计划 */
	fc_final_rel = fetch_upper_rel(fc_root, UPPERREL_FINAL, NULL);
	fc_best_path = get_cheapest_fractional_path(fc_final_rel, fc_tuple_fraction);

	fc_top_plan = create_plan(fc_root, fc_best_path);

	/*
	 * 如果为可滚动游标创建计划，请确保它可以按需向后运行。根据需要在顶部添加一个 Material 节点。
	 */
	if (fc_cursorOptions & CURSOR_OPT_SCROLL)
	{
		if (!ExecSupportsBackwardScan(fc_top_plan))
			fc_top_plan = materialize_finished_plan(fc_top_plan);
	}

	/*
	 * 可选地添加一个 Gather 节点用于测试目的，前提是这样做实际上是安全的。
	 */
	if (force_parallel_mode != FORCE_PARALLEL_OFF && fc_top_plan->parallel_safe)
	{
		Gather	   *fc_gather = makeNode(Gather);

		/*
		 * 顶级计划不得有任何 initPlans，否则它就不应该被标记为并行安全。
		 */
		Assert(fc_top_plan->initPlan == NIL);

		fc_gather->plan.targetlist = fc_top_plan->targetlist;
		fc_gather->plan.qual = NIL;
		fc_gather->plan.lefttree = fc_top_plan;
		fc_gather->plan.righttree = NULL;
		fc_gather->num_workers = 1;
		fc_gather->single_copy = true;
		fc_gather->invisible = (force_parallel_mode == FORCE_PARALLEL_REGRESS);

		/*
		 * 由于此 Gather 没有并行感知的子节点需要信号，因此我们不需要 rescan Param。
		 */
		fc_gather->rescan_param = -1;

		/*
		 * 理想情况下我们应该在这里使用 cost_gather，但
		 * 设置虚拟路径数据以满足它似乎并没有比
		 * 知道它的作用更干净。
		 */
		fc_gather->plan.startup_cost = fc_top_plan->startup_cost +
			parallel_setup_cost;
		fc_gather->plan.total_cost = fc_top_plan->total_cost +
			parallel_setup_cost + parallel_tuple_cost * fc_top_plan->plan_rows;
		fc_gather->plan.plan_rows = fc_top_plan->plan_rows;
		fc_gather->plan.plan_width = fc_top_plan->plan_width;
		fc_gather->plan.parallel_aware = false;
		fc_gather->plan.parallel_safe = false;

		/* 对于并行计划使用并行模式。 */
		fc_root->glob->parallelModeNeeded = true;

		fc_top_plan = &fc_gather->plan;
	}

	/*
	 * 如果生成了任何 Params，则遍历计划树并计算
	 * 每个计划节点的 extParam/allParam 集合。理想情况下
	 * 我们将其合并到 set_plan_references 的树遍历中，
	 * 但现在必须分开，因为我们需要在主计划之前访问子计划，而
	 * 不是在主计划之后。
	 */
	if (fc_glob->paramExecTypes != NIL)
	{
		Assert(list_length(fc_glob->subplans) == list_length(fc_glob->subroots));
		forboth(fc_lp, fc_glob->subplans, fc_lr, fc_glob->subroots)
		{
			Plan	   *fc_subplan = (Plan *) lfirst(fc_lp);
			PlannerInfo *fc_subroot = lfirst_node(PlannerInfo, fc_lr);

			SS_finalize_plan(fc_subroot, fc_subplan);
		}
		SS_finalize_plan(fc_root, fc_top_plan);
	}

	/* 对计划进行最终清理 */
	Assert(fc_glob->finalrtable == NIL);
	Assert(fc_glob->finalrowmarks == NIL);
	Assert(fc_glob->resultRelations == NIL);
	Assert(fc_glob->appendRelations == NIL);
	fc_top_plan = set_plan_references(fc_root, fc_top_plan);
	/* ...和子计划（常规子计划和 initplans） */
	Assert(list_length(fc_glob->subplans) == list_length(fc_glob->subroots));
	forboth(fc_lp, fc_glob->subplans, fc_lr, fc_glob->subroots)
	{
		Plan	   *fc_subplan = (Plan *) lfirst(fc_lp);
		PlannerInfo *fc_subroot = lfirst_node(PlannerInfo, fc_lr);

		lfirst(fc_lp) = set_plan_references(fc_subroot, fc_subplan);
	}

	/* 构建 PlannedStmt 结果 */
	fc_result = makeNode(PlannedStmt);

	fc_result->commandType = fc_parse->commandType;
	fc_result->queryId = fc_parse->queryId;
	fc_result->hasReturning = (fc_parse->returningList != NIL);
	fc_result->hasModifyingCTE = fc_parse->hasModifyingCTE;
	fc_result->canSetTag = fc_parse->canSetTag;
	fc_result->transientPlan = fc_glob->transientPlan;
	fc_result->dependsOnRole = fc_glob->dependsOnRole;
	fc_result->parallelModeNeeded = fc_glob->parallelModeNeeded;
	fc_result->planTree = fc_top_plan;
	fc_result->rtable = fc_glob->finalrtable;
	fc_result->resultRelations = fc_glob->resultRelations;
	fc_result->appendRelations = fc_glob->appendRelations;
	fc_result->subplans = fc_glob->subplans;
	fc_result->rewindPlanIDs = fc_glob->rewindPlanIDs;
	fc_result->rowMarks = fc_glob->finalrowmarks;
	fc_result->relationOids = fc_glob->relationOids;
	fc_result->invalItems = fc_glob->invalItems;
	fc_result->paramExecTypes = fc_glob->paramExecTypes;
	/* utilityStmt 应为 null，但我们不妨复制它 */
	fc_result->utilityStmt = fc_parse->utilityStmt;
	fc_result->stmt_location = fc_parse->stmt_location;
	fc_result->stmt_len = fc_parse->stmt_len;

	fc_result->jitFlags = PGJIT_NONE;
	if (jit_enabled && jit_above_cost >= 0 &&
		fc_top_plan->total_cost > jit_above_cost)
	{
		fc_result->jitFlags |= PGJIT_PERFORM;

		/*
		 * 决定在生成更好的代码上应该投入多少努力。
		 */
		if (jit_optimize_above_cost >= 0 &&
			fc_top_plan->total_cost > jit_optimize_above_cost)
			fc_result->jitFlags |= PGJIT_OPT3;
		if (jit_inline_above_cost >= 0 &&
			fc_top_plan->total_cost > jit_inline_above_cost)
			fc_result->jitFlags |= PGJIT_INLINE;

		/*
		 * 决定哪些操作应该进行 JIT 编译。
		 */
		if (jit_expressions)
			fc_result->jitFlags |= PGJIT_EXPR;
		if (jit_tuple_deforming)
			fc_result->jitFlags |= PGJIT_DEFORM;
	}

	if (fc_glob->partition_directory != NULL)
		DestroyPartitionDirectory(fc_glob->partition_directory);

	return fc_result;
}


/*--------------------
 * subquery_planner
 *	  在子查询上调用规划器。对于查询树中找到的每个
 *	  子 SELECT，我们递归到这里。
 *
 * glob 是当前规划器运行的全局状态。
 * parse 是解析器和重写器生成的查询树。
 * parent_root 是立即父查询的信息（在最顶层为 NULL）。
 * hasRecursion 如果这是一个递归 WITH 查询则为真。
 * tuple_fraction 是我们预计将检索的元组的比例。
 * tuple_fraction 的解释与下面的 grouping_planner 相同。
 *
 * 基本上，这个例程做的是每个查询对象应该只做一次的事情。
 * 然后调用 grouping_planner。在某个时候，
 * grouping_planner 可以在同一个查询对象上递归调用；
 * 目前不是这样，但我们还是保持这两个
 * 例程之间的分隔，以防我们需要在某一天再用它。
 *
 * subquery_planner 将递归调用以处理在查询的表达式和 rangetable
 * 中找到的子查询节点。
 *
 * 返回包含在规划子查询时生成的所有数据的 PlannerInfo 结构（“root”）。
 * 特别是，附加到 (UPPERREL_FINAL, NULL) upperrel 的路径表示我们得出的
 * 实现查询的最便宜方法。最顶层将选择最佳路径，并通过 createplan.c
 * 生成最终计划。
 *--------------------
 */
PlannerInfo *
subquery_planner(PlannerGlobal *fc_glob, Query *fc_parse,
				 PlannerInfo *fc_parent_root,
				 bool fc_hasRecursion, double fc_tuple_fraction)
{
	PlannerInfo *fc_root;
	List	   *fc_newWithCheckOptions;
	List	   *fc_newHaving;
	bool		fc_hasOuterJoins;
	bool		fc_hasResultRTEs;
	RelOptInfo *fc_final_rel;
	ListCell   *fc_l;

	/* 为这个子查询创建 PlannerInfo 数据结构 */
	fc_root = makeNode(PlannerInfo);
	fc_root->parse = fc_parse;
	fc_root->glob = fc_glob;
	fc_root->query_level = fc_parent_root ? fc_parent_root->query_level + 1 : 1;
	fc_root->parent_root = fc_parent_root;
	fc_root->plan_params = NIL;
	fc_root->outer_params = NULL;
	fc_root->planner_cxt = CurrentMemoryContext;
	fc_root->init_plans = NIL;
	fc_root->cte_plan_ids = NIL;
	fc_root->multiexpr_params = NIL;
	fc_root->eq_classes = NIL;
	fc_root->ec_merging_done = false;
	fc_root->all_result_relids =
		fc_parse->resultRelation ? bms_make_singleton(fc_parse->resultRelation) : NULL;
	fc_root->leaf_result_relids = NULL;	/* 我们稍后会了解叶节点情况 */
	fc_root->append_rel_list = NIL;
	fc_root->row_identity_vars = NIL;
	fc_root->rowMarks = NIL;
	memset(fc_root->upper_rels, 0, sizeof(fc_root->upper_rels));
	memset(fc_root->upper_targets, 0, sizeof(fc_root->upper_targets));
	fc_root->processed_tlist = NIL;
	fc_root->update_colnos = NIL;
	fc_root->grouping_map = NULL;
	fc_root->minmax_aggs = NIL;
	fc_root->qual_security_level = 0;
	fc_root->hasPseudoConstantQuals = false;
	fc_root->hasAlternativeSubPlans = false;
	fc_root->hasRecursion = fc_hasRecursion;
	if (fc_hasRecursion)
		fc_root->wt_param_id = assign_special_exec_param(fc_root);
	else
		fc_root->wt_param_id = -1;
	fc_root->non_recursive_path = NULL;
	fc_root->partColsUpdated = false;

	/*
	 * 如果有 WITH 列表，处理每个 WITH 查询并将其
	 * 转换为 RTE_SUBQUERY RTE(s) 或为其构建 initplan SubPlan 结构。
	 */
	if (fc_parse->cteList)
		SS_process_ctes(fc_root);

	/*
	 * 如果是 MERGE 命令，则根据需要转换连接列表。
	 */
	transform_MERGE_to_join(fc_parse);

	/*
	 * 如果 FROM 子句为空，用一个虚拟的 RTE_RESULT RTE 替换它，
	 * 以便我们不需要处理那么多特殊情况。
	 */
	replace_empty_jointree(fc_parse);

	/*
	 * 在 WHERE 和 JOIN/ON 子句中查找 ANY 和 EXISTS 子链接，尝试
	 * 将它们转换为连接。请注意，这一步不递归进入
	 * 子查询；如果我们拉起任何子查询，其子链接将在拉起之前处理。
	 */
	if (fc_parse->hasSubLinks)
		pull_up_sublinks(fc_root);

	/*
	 * 扫描 rangetable 查找函数 RTEs，对它们进行常量简化，
	 * 然后如果可能将其内联（生成下一个可能被拉起的子查询）。
	 * 此处的递归问题与子链接的处理方式相同。
	 */
	preprocess_function_rtes(fc_root);

	/*
	 * 检查联合树中的任何子查询是否可以合并到此
	 * 查询中。
	 */
	pull_up_subqueries(fc_root);

	/* 如果这是一个简单的 UNION ALL 查询，将其展平为 appendrel。
我们这样做是因为它需要将 pull_up_subqueries 应用于 UNION ALL 的叶子查询，而这些查询在上面没有被处理，因为它们没有被联合树引用（在我们这样做之后它们会被引用）。 */
	if (fc_parse->setOperations)
		flatten_simple_union_all(fc_root);

	/*
	 * 调查 rangetable 以查看有哪些类型的条目存在。如果没有使用相关的 SQL 特性，我们可以
	 * 跳过某些后续处理；例如，如果没有 JOIN RTE，我们可以避免执行
	 * flatten_join_alias_vars() 的开销。这必须在我们完成
	 * 添加 rangetable 条目之后进行，当然。（注意：实际上，处理
	 * 继承或分区关系可能会导致其子表的 RTE 后来被添加；但那些必须都是 RTE_RELATION 条目，因此它们
	 * 不会使这里得出的结论失效。）
	 */
	fc_root->hasJoinRTEs = false;
	fc_root->hasLateralRTEs = false;
	fc_hasOuterJoins = false;
	fc_hasResultRTEs = false;
	foreach(fc_l, fc_parse->rtable)
	{
		RangeTblEntry *fc_rte = lfirst_node(RangeTblEntry, fc_l);

		switch (fc_rte->rtekind)
		{
			case RTE_RELATION:
				if (fc_rte->inh)
				{
					/*
					 * 检查关系是否实际上有任何子项；
					 * 如果没有，则清除 inh 标志，以便我们可以将其视为
					 * 普通基本关系。
					 *
					 * 注意：如果关系曾经有子项但现在没有，这可能会给出误报结果。
					 * 我们曾经能够在发现这一点后清除 rte->inh，
					 * 但现在不再可以；我们必须将这种情况作为
					 * 完整的继承来处理。
					 */
					fc_rte->inh = has_subclass(fc_rte->relid);
				}
				break;
			case RTE_JOIN:
				fc_root->hasJoinRTEs = true;
				if (IS_OUTER_JOIN(fc_rte->jointype))
					fc_hasOuterJoins = true;
				break;
			case RTE_RESULT:
				fc_hasResultRTEs = true;
				break;
			default:
				/* 其他 RTE 类型在这里没有工作 */
				break;
		}

		if (fc_rte->lateral)
			fc_root->hasLateralRTEs = true;

		/*
		 * 我们现在还可以确定任何
		 * securityQuals所需的最大安全级别。继承子RTE的增加不会影响
		 * 这一点，因为子表没有自己的securityQuals；见
		 * expand_single_inheritance_child()。
		 */
		if (fc_rte->securityQuals)
			fc_root->qual_security_level = Max(fc_root->qual_security_level,
											list_length(fc_rte->securityQuals));
	}

	/*
	 * 如果我们现在确认查询目标关系是非继承的，则将其标记为叶子目标。
	 */
	if (fc_parse->resultRelation)
	{
		RangeTblEntry *fc_rte = rt_fetch(fc_parse->resultRelation, fc_parse->rtable);

		if (!fc_rte->inh)
			fc_root->leaf_result_relids =
				bms_make_singleton(fc_parse->resultRelation);
	}

	/*
	 * 预处理RowMark信息。我们需要在子查询上提取之后进行此操作，以便所有基关系都存在。
	 */
	fc_preprocess_rowmarks(fc_root);

	/*
	 * 设置hasHavingQual以记住HAVING子句是否存在。需要这样做是因为preprocess_expression将一个恒定为真的条件简化为空的qual列表……但“HAVING TRUE”不是一个语义无操作。
	 */
	fc_root->hasHavingQual = (fc_parse->havingQual != NULL);

	/*
	 * 对目标列表和 qual，以及查询树中的其他随机表达式进行表达式预处理。请注意，我们不需要显式处理排序/分组表达式，因为它们实际上是目标列表的一部分。
	 */
	fc_parse->targetList = (List *)
		fc_preprocess_expression(fc_root, (Node *) fc_parse->targetList,
							  EXPRKIND_TARGET);

	/* 常量折叠可能已经移除了所有返回集合的函数 */
	if (fc_parse->hasTargetSRFs)
		fc_parse->hasTargetSRFs = expression_returns_set((Node *) fc_parse->targetList);

	fc_newWithCheckOptions = NIL;
	foreach(fc_l, fc_parse->withCheckOptions)
	{
		WithCheckOption *fc_wco = lfirst_node(WithCheckOption, fc_l);

		fc_wco->qual = fc_preprocess_expression(fc_root, fc_wco->qual,
										  EXPRKIND_QUAL);
		if (fc_wco->qual != NULL)
			fc_newWithCheckOptions = lappend(fc_newWithCheckOptions, fc_wco);
	}
	fc_parse->withCheckOptions = fc_newWithCheckOptions;

	fc_parse->returningList = (List *)
		fc_preprocess_expression(fc_root, (Node *) fc_parse->returningList,
							  EXPRKIND_TARGET);

	fc_preprocess_qual_conditions(fc_root, (Node *) fc_parse->jointree);

	fc_parse->havingQual = fc_preprocess_expression(fc_root, fc_parse->havingQual,
											  EXPRKIND_QUAL);

	foreach(fc_l, fc_parse->windowClause)
	{
		WindowClause *fc_wc = lfirst_node(WindowClause, fc_l);

		/* partitionClause/orderClause是排序/分组表达式 */
		fc_wc->startOffset = fc_preprocess_expression(fc_root, fc_wc->startOffset,
												EXPRKIND_LIMIT);
		fc_wc->endOffset = fc_preprocess_expression(fc_root, fc_wc->endOffset,
											  EXPRKIND_LIMIT);
		fc_wc->runCondition = (List *) fc_preprocess_expression(fc_root,
														  (Node *) fc_wc->runCondition,
														  EXPRKIND_TARGET);
	}

	fc_parse->limitOffset = fc_preprocess_expression(fc_root, fc_parse->limitOffset,
											   EXPRKIND_LIMIT);
	fc_parse->limitCount = fc_preprocess_expression(fc_root, fc_parse->limitCount,
											  EXPRKIND_LIMIT);

	if (fc_parse->onConflict)
	{
		fc_parse->onConflict->arbiterElems = (List *)
			fc_preprocess_expression(fc_root,
								  (Node *) fc_parse->onConflict->arbiterElems,
								  EXPRKIND_ARBITER_ELEM);
		fc_parse->onConflict->arbiterWhere =
			fc_preprocess_expression(fc_root,
								  fc_parse->onConflict->arbiterWhere,
								  EXPRKIND_QUAL);
		fc_parse->onConflict->onConflictSet = (List *)
			fc_preprocess_expression(fc_root,
								  (Node *) fc_parse->onConflict->onConflictSet,
								  EXPRKIND_TARGET);
		fc_parse->onConflict->onConflictWhere =
			fc_preprocess_expression(fc_root,
								  fc_parse->onConflict->onConflictWhere,
								  EXPRKIND_QUAL);
		/* exclRelTlist 仅包含 Vars，因此无需预处理 */
	}

	foreach(fc_l, fc_parse->mergeActionList)
	{
		MergeAction *fc_action = (MergeAction *) lfirst(fc_l);

		fc_action->targetList = (List *)
			fc_preprocess_expression(fc_root,
								  (Node *) fc_action->targetList,
								  EXPRKIND_TARGET);
		fc_action->qual =
			fc_preprocess_expression(fc_root,
								  (Node *) fc_action->qual,
								  EXPRKIND_QUAL);
	}

	fc_root->append_rel_list = (List *)
		fc_preprocess_expression(fc_root, (Node *) fc_root->append_rel_list,
							  EXPRKIND_APPINFO);

	/* 还需要对 RTE 中的表达式进行预处理 */
	foreach(fc_l, fc_parse->rtable)
	{
		RangeTblEntry *fc_rte = lfirst_node(RangeTblEntry, fc_l);
		int			fc_kind;
		ListCell   *fc_lcsq;

		if (fc_rte->rtekind == RTE_RELATION)
		{
			if (fc_rte->tablesample)
				fc_rte->tablesample = (TableSampleClause *)
					fc_preprocess_expression(fc_root,
										  (Node *) fc_rte->tablesample,
										  EXPRKIND_TABLESAMPLE);
		}
		else if (fc_rte->rtekind == RTE_SUBQUERY)
		{
			/*
			 * 我们暂时不想对子查询的表达式进行所有预处理，
			 * 因为这将在我们计划时发生。 但是如果它包含任何
			 * 我们级别的连接别名，则必须立即扩展这些别名，
			 * 因为子查询的规划不会执行此操作。
			 * 这只有在子查询为 LATERAL 时才有可能。
			 */
			if (fc_rte->lateral && fc_root->hasJoinRTEs)
				fc_rte->subquery = (Query *)
					flatten_join_alias_vars(fc_root->parse,
											(Node *) fc_rte->subquery);
		}
		else if (fc_rte->rtekind == RTE_FUNCTION)
		{
			/* 完全预处理函数表达式 */
			fc_kind = fc_rte->lateral ? EXPRKIND_RTFUNC_LATERAL : EXPRKIND_RTFUNC;
			fc_rte->functions = (List *)
				fc_preprocess_expression(fc_root, (Node *) fc_rte->functions, fc_kind);
		}
		else if (fc_rte->rtekind == RTE_TABLEFUNC)
		{
			/* 完全预处理函数表达式 */
			fc_kind = fc_rte->lateral ? EXPRKIND_TABLEFUNC_LATERAL : EXPRKIND_TABLEFUNC;
			fc_rte->tablefunc = (TableFunc *)
				fc_preprocess_expression(fc_root, (Node *) fc_rte->tablefunc, fc_kind);
		}
		else if (fc_rte->rtekind == RTE_VALUES)
		{
			/* 完全预处理值列表 */
			fc_kind = fc_rte->lateral ? EXPRKIND_VALUES_LATERAL : EXPRKIND_VALUES;
			fc_rte->values_lists = (List *)
				fc_preprocess_expression(fc_root, (Node *) fc_rte->values_lists, fc_kind);
		}

		/*
		 * 将 securityQuals 列表中的每个元素按照
		 * 单独的 qual 表达式进行处理（实际上确实如此）。我们需要这样做来获得
		 * AND/OR 结构的适当规范化。请注意，这将每个元素转换为一个隐式的 AND 子列表。
		 */
		foreach(fc_lcsq, fc_rte->securityQuals)
		{
			lfirst(fc_lcsq) = fc_preprocess_expression(fc_root,
												 (Node *) lfirst(fc_lcsq),
												 EXPRKIND_QUAL);
		}
	}

	/*
	 * 现在我们完成了表达式的预处理，特别是完成了连接别名变量的扁平化，去掉 joinaliasvars 列表。
	 * 它们不再与树中其余部分的表达式匹配，因为我们没有预处理这些列表中的表达式（也不想这样做；例如，扩展 SubLink 将导致
	 * 无用的未引用子计划）。保留它们只会给稍后扫描树带来危害。我们可以尝试通过在此之后进行的每次树扫描中使用 QTW_IGNORE_JOINALIASES 来防止这种情况，
	 * 但这听起来并不是非常可靠。
	 */
	if (fc_root->hasJoinRTEs)
	{
		foreach(fc_l, fc_parse->rtable)
		{
			RangeTblEntry *fc_rte = lfirst_node(RangeTblEntry, fc_l);

			fc_rte->joinaliasvars = NIL;
		}
	}

	/*
	 * 在某些情况下，我们可能希望将 HAVING 子句转移到 WHERE。 
	 * 如果 HAVING 子句包含聚合（显然）或易失性函数， 
	 * 我们就不能这样做（因为 HAVING 子句应该每组只执行一次）。 
	 * 如果有任何非空分组集，我们也无法这样做；将此类子句 
	 * 移动到 WHERE 可能会更改结果，如果任何引用的列 
	 * 不存在于所有分组集中。 
	 * （如果只有空分组集，则 HAVING 子句必须是 
	 * 退化的，如下所述。）
	 *
	 * 此外，子句的执行开销可能如此之大，以至于我们 
	 * 最好每组只执行一次，尽管失去了选择性。 
	 * 除非我们将整个规划过程执行两次，否则很难估计 
	 * 这一点，因此我们使用一个启发式：包含 
	 * 子计划的子句留在 HAVING 中。 
	 * 否则，我们将 HAVING 子句移动或复制到 WHERE， 
	 * 希望在聚合之前消除元组，而不是之后。
	 *
	 * 如果查询具有显式分组，则我们可以简单地将此类 
	 * 子句移动到 WHERE；任何未通过该子句的组 
	 * 将不会出现在输出中，因为它的元组不会到达 
	 * 分组或聚合阶段。 
	 * 否则，我们必须有一个退化的（无变量） 
	 * HAVING 子句，将其放入 WHERE，以便 query_planner() 
	 * 可以在一个门控的 Result 节点中使用它，但也保持在 
	 * HAVING 中以确保我们不会发出无效的聚合行。 
	 * （这可以做得更好，但似乎没有值得优化的。）
	 *
	 * 注意，havingQual 和 parse->jointree->quals 在这一点上 
	 * 都以隐式 AND 的列表形式存在，尽管它们被声明为 Node *。
	 */
	fc_newHaving = NIL;
	foreach(fc_l, (List *) fc_parse->havingQual)
	{
		Node	   *fc_havingclause = (Node *) lfirst(fc_l);

		if ((fc_parse->groupClause && fc_parse->groupingSets) ||
			contain_agg_clause(fc_havingclause) ||
			contain_volatile_functions(fc_havingclause) ||
			contain_subplans(fc_havingclause))
		{
			/* 保持在 HAVING 中 */
			fc_newHaving = lappend(fc_newHaving, fc_havingclause);
		}
		else if (fc_parse->groupClause && !fc_parse->groupingSets)
		{
			/* 移动到 WHERE */
			fc_parse->jointree->quals = (Node *)
				lappend((List *) fc_parse->jointree->quals, fc_havingclause);
		}
		else
		{
			/* 在 WHERE 中放一个副本，保留在 HAVING 中 */
			fc_parse->jointree->quals = (Node *)
				lappend((List *) fc_parse->jointree->quals,
						copyObject(fc_havingclause));
			fc_newHaving = lappend(fc_newHaving, fc_havingclause);
		}
	}
	fc_parse->havingQual = (Node *) fc_newHaving;

	/* 删除任何冗余的 GROUP BY 列 */
	fc_remove_useless_groupby_columns(fc_root);

	/*
	 * 如果我们有任何外连接，尝试将它们减少为普通的内部连接。 
	 * 在我们完成表达式预处理之后，这一步是最容易完成的。
	 */
	if (fc_hasOuterJoins)
		reduce_outer_joins(fc_root);

	/*
	 * 如果我们有任何 RTE_RESULT 关系，检查它们是否可以从 
	 * 联合树中删除。这一步在我们完成表达式预处理 
	 * 和外连接简化之后最有效。
	 */
	if (fc_hasResultRTEs)
		remove_useless_result_rtes(fc_root);

	/*
	 * 执行主要规划。
	 */
	fc_grouping_planner(fc_root, fc_tuple_fraction);

	/*
	 * 捕获我们可以访问的外部参数 ID 的集合，以便稍后用于 
	 * extParam/allParam 计算。
	 */
	SS_identify_outer_params(fc_root);

	/*
	 * 如果在此查询级别创建了任何 initPlans，调整存活的 
	 * Paths 的成本和并行安全标志以考虑它们。 
	 * initPlans 直到 create_plan() 运行后才会实际附加到 
	 * 计划树，但我们必须现在包括它们的影响。
	 */
	fc_final_rel = fetch_upper_rel(fc_root, UPPERREL_FINAL, NULL);
	SS_charge_for_initplans(fc_root, fc_final_rel);

	
/*
	 * 确保我们已经为最终的关系识别到了最便宜的路径。 （通过在这里而不是在 grouping_planner 中执行这一操作，我们将 initPlan 成本纳入决策，尽管这不太可能改变任何事情。）
	 */
	set_cheapest(fc_final_rel);

	return fc_root;
}

/*
 * preprocess_expression
 *		对表达式进行 subquery_planner 的预处理工作，
 *		该表达式可以是目标列表、WHERE 子句（包括 JOIN/ON
 *		条件）、HAVING 子句或其他几个内容。
 */
static Node * fc_preprocess_expression(PlannerInfo *fc_root, Node *fc_expr, int fc_kind)
{
	/*
	 * 如果表达式为空则快速退出。这种情况发生得足够多
	 * 以至于值得检查。注意 null->null 也是隐式-AND 结果格式的正确转换。
	 */
	if (fc_expr == NULL)
		return NULL;

	/*
	 * 如果查询有任何连接 RTE，替换连接别名变量为
	 * 基础关系变量。我们必须首先执行此操作，因为我们可能从 joinaliasvars 列表中提取的任何表达式还没有经过预处理。
	 * 例如，如果我们在 sublink 处理后执行此操作，从连接别名展开的 sublinks 将无法被处理。但在非侧面 RTE 函数、VALUES 列表和 TABLESAMPLE 子句中我们可以跳过此步骤，因为它们不能包含当前查询级别的任何变量。
	 */
	if (fc_root->hasJoinRTEs &&
		!(fc_kind == EXPRKIND_RTFUNC ||
		  fc_kind == EXPRKIND_VALUES ||
		  fc_kind == EXPRKIND_TABLESAMPLE ||
		  fc_kind == EXPRKIND_TABLEFUNC))
		fc_expr = flatten_join_alias_vars(fc_root->parse, fc_expr);

	/*
	 * 简化常量表达式。对于函数 RTE，这已经由 preprocess_function_rtes 完成了。
	 * （但请注意，对于 EXPRKIND_RTFUNC_LATERAL，我们必须再次执行此操作，因为这些可能已包含通过展平子查询或连接别名变量插入的未简化的子表达式。）
	 *
	 * 注意：此操作的一个基本效果是将命名参数函数调用转换为位置表示法，并插入任何默认参数的当前实际值。为了确保这一点，我们必须在这里处理所有表达式。以前的 PG 版本有时会跳过常量简化，如果看起来不值得麻烦，但我们现在不能再这样做了。
	 *
	 * 注意：这还会将嵌套的 AND 和 OR 表达式展平为 N 个参数的形式。在此之后对量表达式的所有处理必须小心保持 AND/OR 的平坦性——也就是说，不要生成一个直接在 AND 下的 AND 树，也不要在 OR 下直接生成 OR。
	 */
	if (fc_kind != EXPRKIND_RTFUNC)
		fc_expr = eval_const_expressions(fc_root, fc_expr);

	/*
	 * 如果它是一个量或 havingQual，则规范化它。
	 */
	if (fc_kind == EXPRKIND_QUAL)
	{
		fc_expr = (Node *) canonicalize_qual((Expr *) fc_expr, false);

#ifdef OPTIMIZER_DEBUG
		printf("After canonicalize_qual()\n");
		pprint(fc_expr);
#endif
	}

	/*
	 * 检查是否有任何带有常量数组的 ScalarArrayOpExpr，并设置
	 * 任何可能通过使用哈希查找而比线性搜索更快执行的 hashfuncid。
	 */
	if (fc_kind == EXPRKIND_QUAL || fc_kind == EXPRKIND_TARGET)
	{
		convert_saop_to_hashed_saop(fc_expr);
	}

	/* 展开 SubLinks 为 SubPlans */
	if (fc_root->parse->hasSubLinks)
		fc_expr = SS_process_sublinks(fc_root, fc_expr, (fc_kind == EXPRKIND_QUAL));

	/*
	 * XXX 除非你已经理解了 SS_replace_correlation_vars 中的注释，
	 * 否则不要在这里插入任何内容…
	 */

	/* 用 Param 节点替换 uplevel 变量（这在 VALUES 中是可能的） */
	if (fc_root->query_level > 1)
		fc_expr = SS_replace_correlation_vars(fc_root, fc_expr);

	/*
	 * 如果它是一个量或 havingQual，则将其转换为隐式-AND 格式。 （我们
	 * 不希望在 eval_const_expressions 之前执行此操作，因为后者将无法正确简化顶层 AND。此外，
	 * SS_process_sublinks 期望显式-AND 格式。）
	 */
	if (fc_kind == EXPRKIND_QUAL)
		fc_expr = (Node *) make_ands_implicit((Expr *) fc_expr);

	return fc_expr;
}

/*
 * preprocess_qual_conditions
 *		递归扫描查询的联合树，并对其中找到的每个 qual 条件执行子查询计划程序的预处理工作。
 */
static void fc_preprocess_qual_conditions(PlannerInfo *fc_root, Node *fc_jtnode)
{
	if (fc_jtnode == NULL)
		return;
	if (IsA(fc_jtnode, RangeTblRef))
	{
		/* 此处无操作 */
	}
	else if (IsA(fc_jtnode, FromExpr))
	{
		FromExpr   *fc_f = (FromExpr *) fc_jtnode;
		ListCell   *fc_l;

		foreach(fc_l, fc_f->fromlist)
			fc_preprocess_qual_conditions(fc_root, lfirst(fc_l));

		fc_f->quals = fc_preprocess_expression(fc_root, fc_f->quals, EXPRKIND_QUAL);
	}
	else if (IsA(fc_jtnode, JoinExpr))
	{
		JoinExpr   *fc_j = (JoinExpr *) fc_jtnode;

		fc_preprocess_qual_conditions(fc_root, fc_j->larg);
		fc_preprocess_qual_conditions(fc_root, fc_j->rarg);

		fc_j->quals = fc_preprocess_expression(fc_root, fc_j->quals, EXPRKIND_QUAL);
	}
	else
		elog(ERROR, "unrecognized node type: %d",
			 (int) nodeTag(fc_jtnode));
}

/*
 * preprocess_phv_expression
 *	  对被提升的 PlaceHolderVar 表达式进行预处理。
 *
 * 如果一个 LATERAL 子查询引用了另一个子查询的输出，而由于中间的外连接，必须将该输出封装在 PlaceHolderVar 中，那么我们将把 PlaceHolderVar 表达式推入子查询，并在 find_lateral_references 过程中将其拉回，该过程在子查询计划程序预处理了当前查询级别中的所有表达式后运行。因此我们需要在那时进行预处理。
 */
Expr * preprocess_phv_expression(PlannerInfo *fc_root, Expr *fc_expr)
{
	return (Expr *) fc_preprocess_expression(fc_root, (Node *) fc_expr, EXPRKIND_PHV);
}

/*--------------------
 * grouping_planner
 *	  执行与分组、聚合等相关的规划步骤。
 *
 * 此函数将所有必需的顶级处理添加到查询计划程序生成的扫描/连接路径中。
 *
 * tuple_fraction 是我们期望被检索的元组的比例。
 * tuple_fraction 的解释如下：
 *	  0: 期待检索所有元组（正常情况）
 *	  0 < tuple_fraction < 1: 期望从计划中检索给定比例的元组
 *	  tuple_fraction >= 1: tuple_fraction 是预期检索的元组的绝对数量（即 LIMIT 规范）
 *
 * 不返回任何内容；有用的输出在我们附加到 *root 中的 (UPPERREL_FINAL, NULL) upperrel 的路径中。此外，
 * root->processed_tlist 包含最终处理的目标列表。
 *
 * 请注意，我们尚未对最终关系执行 set_cheapest()；将这一点留给调用者是方便的。
 *--------------------
 */
static void fc_grouping_planner(PlannerInfo *fc_root, double fc_tuple_fraction)
{
	Query	   *fc_parse = fc_root->parse;
	int64		fc_offset_est = 0;
	int64		fc_count_est = 0;
	double		fc_limit_tuples = -1.0;
	bool		fc_have_postponed_srfs = false;
	PathTarget *fc_final_target;
	List	   *fc_final_targets;
	List	   *fc_final_targets_contain_srfs;
	bool		fc_final_target_parallel_safe;
	RelOptInfo *fc_current_rel;
	RelOptInfo *fc_final_rel;
	FinalPathExtraData fc_extra;
	ListCell   *fc_lc;

	/* 如果有 LIMIT/OFFSET，调整调用者提供的 tuple_fraction */
	if (fc_parse->limitCount || fc_parse->limitOffset)
	{
		fc_tuple_fraction = fc_preprocess_limit(fc_root, fc_tuple_fraction,
										  &fc_offset_est, &fc_count_est);

		/*
		 * 如果我们有已知的 LIMIT，并且没有未知的 OFFSET，我们可以
		 * 估算使用带界限的排序的效果。
		 */
		if (fc_count_est > 0 && fc_offset_est >= 0)
			fc_limit_tuples = (double) fc_count_est + (double) fc_offset_est;
	}

	/* 让 lower-level 例程可以访问 tuple_fraction */
	fc_root->tuple_fraction = fc_tuple_fraction;

	if (fc_parse->setOperations)
	{
		/*
		 * 如果有顶级的 ORDER BY，假设我们必须获取所有
		 * 元组。鉴于下面所有可能避免排序的黑客，这可能过于简单；但这里准确估算的概率反正很低。XXX 尝试去掉这个，以支持让 plan_set_operations 生成快速开始和
		 * 最便宜的总路径。
		 */
		if (fc_parse->sortClause)
			fc_root->tuple_fraction = 0.0;

		/*
		 * 为集合操作构造路径。结果将不需要任何
		 * 工作，除了可能的顶层排序和/或 LIMIT。请注意，任何
		 * 针对递归联合的特殊工作都是计划集合操作的责任。
		 */
		fc_current_rel = plan_set_operations(fc_root);

		/*
		 * 我们不应该需要调用 preprocess_targetlist，因为我们必须在一个 SELECT 查询节点中。
		 * 相反，使用 plan_set_operations 返回的 processed_tlist（因为这可以告诉它是否返回了任何 resjunk 列！），并从原始目标列表中转移任何排序键信息。
		 */
		Assert(fc_parse->commandType == CMD_SELECT);

		/* 为了安全起见，复制 processed_tlist 而不是就地修改 */
		fc_root->processed_tlist =
			fc_postprocess_setop_tlist(copyObject(fc_root->processed_tlist),
									fc_parse->targetList);

		/* 还提取集合操作结果目标列表的 PathTarget 形式 */
		fc_final_target = fc_current_rel->cheapest_total_path->pathtarget;

		/* 并检查它是否是并行安全的 */
		fc_final_target_parallel_safe =
			is_parallel_safe(fc_root, (Node *) fc_final_target->exprs);

		/* 集合操作结果目标列表不能包含任何 SRFs */
		Assert(!fc_parse->hasTargetSRFs);
		fc_final_targets = fc_final_targets_contain_srfs = NIL;

		/*
		 * 这里无法处理 FOR [KEY] UPDATE/SHARE（解析器应该已经
		 * 检查过，但让我们确保）。
		 */
		if (fc_parse->rowMarks)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
			/*------
			  翻译者：%s 是一个 SQL 行锁定子句，如 FOR UPDATE */
					 errmsg("%s is not allowed with UNION/INTERSECT/EXCEPT",
							LCS_asString(linitial_node(RowMarkClause,
													   fc_parse->rowMarks)->strength))));

		/*
		 * 计算表示结果排序要求的路径键
		 */
		Assert(fc_parse->distinctClause == NIL);
		fc_root->sort_pathkeys = make_pathkeys_for_sortclauses(fc_root,
															fc_parse->sortClause,
															fc_root->processed_tlist);
	}
	else
	{
		/* 没有集合操作，进行常规规划 */
		PathTarget *sort_input_target;
		List	   *sort_input_targets;
		List	   *sort_input_targets_contain_srfs;
		bool		sort_input_target_parallel_safe;
		PathTarget *fc_grouping_target;
		List	   *fc_grouping_targets;
		List	   *fc_grouping_targets_contain_srfs;
		bool		fc_grouping_target_parallel_safe;
		PathTarget *fc_scanjoin_target;
		List	   *fc_scanjoin_targets;
		List	   *fc_scanjoin_targets_contain_srfs;
		bool		fc_scanjoin_target_parallel_safe;
		bool		fc_scanjoin_target_same_exprs;
		bool		fc_have_grouping;
		WindowFuncLists *fc_wflists = NULL;
		List	   *fc_activeWindows = NIL;
		grouping_sets_data *fc_gset_data = NULL;
		standard_qp_extra fc_qp_extra;

		/* 递归查询应该始终具有 setOperations */
		Assert(!fc_root->hasRecursion);

		/* 如果有，预处理分组集和 GROUP BY 子句 */
		if (fc_parse->groupingSets)
		{
			fc_gset_data = fc_preprocess_grouping_sets(fc_root);
		}
		else
		{
			/* 如果有，预处理常规 GROUP BY 子句 */
			if (fc_parse->groupClause)
				fc_parse->groupClause = fc_preprocess_groupclause(fc_root, NIL);
		}

		/*
		 * 预处理目标列表。请注意，剩下的大部分规划
		 * 工作将使用 tlists 的 PathTarget 表示，但
		 * 我们还必须保持最终目标列表的完整表示，以便
		 * 我们可以将其装饰（resnames 等）转移到完成计划的最上层
		 * 目标列表中。这保存在 processed_tlist 中。
		 */
		preprocess_targetlist(fc_root);

		/*
		 * 标记所有具有解析的 aggtranstypes 的聚合，并检测
		 * 重复或可以共享过渡状态的聚合。我们
		 * 必须在将目标列表切片和切分为各种路径目标之前执行此操作，否则某些 Aggref 节点的副本可能会逃避被标记。
		 */
		if (fc_parse->hasAggs)
		{
			preprocess_aggrefs(fc_root, (Node *) fc_root->processed_tlist);
			preprocess_aggrefs(fc_root, (Node *) fc_parse->havingQual);
		}

		/*
		 * 在目标列表中定位任何窗口函数。（我们不需要在
		 * 其他地方查找，因为用于 ORDER BY 的表达式也会在其中。）请注意，它们可能都已通过常量
		 * 折叠被消除，在这种情况下我们不需要再执行任何工作。
		 */
		if (fc_parse->hasWindowFuncs)
		{
			fc_wflists = find_window_functions((Node *) fc_root->processed_tlist,
											list_length(fc_parse->windowClause));
			if (fc_wflists->numWindowFuncs > 0)
				fc_activeWindows = fc_select_active_windows(fc_root, fc_wflists);
			else
				fc_parse->hasWindowFuncs = false;
		}

		
/*
		 * 预处理 MIN/MAX 聚合，如果有的话。注意：在这里和 query_planner() 调用之间添加逻辑时要小心。
		 * 任何在 MIN/MAX 可优化情况下所需的内容必须在 planagg.c 中重复。
		 */
		if (fc_parse->hasAggs)
			preprocess_minmax_aggregates(fc_root);

		/*
		 * 确定查询规划器的结果子计划需要返回的行数是否有硬限制。
		 * 即使我们知道总体硬限制，如果查询有任何分组/聚合操作或 SRFs 在 tlist 中，则不适用。
		 */
		if (fc_parse->groupClause ||
			fc_parse->groupingSets ||
			fc_parse->distinctClause ||
			fc_parse->hasAggs ||
			fc_parse->hasWindowFuncs ||
			fc_parse->hasTargetSRFs ||
			fc_root->hasHavingQual)
			fc_root->limit_tuples = -1.0;
		else
			fc_root->limit_tuples = fc_limit_tuples;

		/* 设置 standard_qp_callback 所需的数据 */
		fc_qp_extra.activeWindows = fc_activeWindows;
		fc_qp_extra.groupClause = (fc_gset_data
								? (fc_gset_data->rollups ? linitial_node(RollupData, fc_gset_data->rollups)->groupClause : NIL)
								: fc_parse->groupClause);

		/*
		 * 为此查询的扫描/连接部分生成最佳的未排序和预排序路径，
		 * 即由 FROM/WHERE 子句表示的处理。
		 * （注意，可能没有任何预排序路径。）
		 * 我们还在 standard_qp_callback 中生成查询排序子句、不同子句等的 pathkey 表示。
		 */
		fc_current_rel = query_planner(fc_root, fc_standard_qp_callback, &fc_qp_extra);

		/*
		 * 将查询的结果 tlist 转换为 PathTarget 格式。
		 *
		 * 注意：在 query_planner() 执行 appendrel 扩展之前无法做到这一点，
		 * 因为这可能会将 resjunk 条目添加到 root->processed_tlist。
		 * 等待之后也是有帮助的，因为目标宽度估计可以使用在 query_planner() 中获得的按 Var 宽度的数字。
		 */
		fc_final_target = create_pathtarget(fc_root, fc_root->processed_tlist);
		fc_final_target_parallel_safe =
			is_parallel_safe(fc_root, (Node *) fc_final_target->exprs);

		/*
		 * 如果给出了 ORDER BY，考虑我们是否应该使用后排序投影，
		 * 并在必要时计算前一步的调整目标。
		 */
		if (fc_parse->sortClause)
		{
			sort_input_target = fc_make_sort_input_target(fc_root,
													   fc_final_target,
													   &fc_have_postponed_srfs);
			sort_input_target_parallel_safe =
				is_parallel_safe(fc_root, (Node *) sort_input_target->exprs);
		}
		else
		{
			sort_input_target = fc_final_target;
			sort_input_target_parallel_safe = fc_final_target_parallel_safe;
		}

		/*
		 * 如果我们需要处理窗口函数，则任何分组步骤的输出需要是窗口函数所需的内容；
		 * 否则，它应该是 sort_input_target。
		 */
		if (fc_activeWindows)
		{
			fc_grouping_target = fc_make_window_input_target(fc_root,
													   fc_final_target,
													   fc_activeWindows);
			fc_grouping_target_parallel_safe =
				is_parallel_safe(fc_root, (Node *) fc_grouping_target->exprs);
		}
		else
		{
			fc_grouping_target = sort_input_target;
			fc_grouping_target_parallel_safe = sort_input_target_parallel_safe;
		}

		/*
		 * 如果我们需要进行分组或聚合，则最上层的扫描/连接
		 * 计划节点必须发出分组步骤所需的内容；否则，它
		 * 应该发出 grouping_target。
		 */
		fc_have_grouping = (fc_parse->groupClause || fc_parse->groupingSets ||
						 fc_parse->hasAggs || fc_root->hasHavingQual);
		if (fc_have_grouping)
		{
			fc_scanjoin_target = fc_make_group_input_target(fc_root, fc_final_target);
			fc_scanjoin_target_parallel_safe =
				is_parallel_safe(fc_root, (Node *) fc_scanjoin_target->exprs);
		}
		else
		{
			fc_scanjoin_target = fc_grouping_target;
			fc_scanjoin_target_parallel_safe = fc_grouping_target_parallel_safe;
		}

		/*
		 * 如果在目标列表中存在任何 SRF，我们必须将这些
		 * PathTargets 分离为 SRF 计算目标和非 SRF 目标。 
		 * 将每个命名目标替换为一个非 SRF 版本，并记住
		 * 我们需要在之后添加的额外投影步骤列表。
		 */
		if (fc_parse->hasTargetSRFs)
		{
			/* final_target 不重新计算 sort_input_target 中的任何 SRF */
			split_pathtarget_at_srfs(fc_root, fc_final_target, sort_input_target,
									 &fc_final_targets,
									 &fc_final_targets_contain_srfs);
			fc_final_target = linitial_node(PathTarget, fc_final_targets);
			Assert(!linitial_int(fc_final_targets_contain_srfs));
			/* sort_input_target 与 grouping_target 相同 */
			split_pathtarget_at_srfs(fc_root, sort_input_target, fc_grouping_target,
									 &sort_input_targets,
									 &sort_input_targets_contain_srfs);
			sort_input_target = linitial_node(PathTarget, sort_input_targets);
			Assert(!linitial_int(sort_input_targets_contain_srfs));
			/* grouping_target 与 scanjoin_target 相同 */
			split_pathtarget_at_srfs(fc_root, fc_grouping_target, fc_scanjoin_target,
									 &fc_grouping_targets,
									 &fc_grouping_targets_contain_srfs);
			fc_grouping_target = linitial_node(PathTarget, fc_grouping_targets);
			Assert(!linitial_int(fc_grouping_targets_contain_srfs));
			/* scanjoin_target 将不会提前计算任何 SRF */
			split_pathtarget_at_srfs(fc_root, fc_scanjoin_target, NULL,
									 &fc_scanjoin_targets,
									 &fc_scanjoin_targets_contain_srfs);
			fc_scanjoin_target = linitial_node(PathTarget, fc_scanjoin_targets);
			Assert(!linitial_int(fc_scanjoin_targets_contain_srfs));
		}
		else
		{
			/* 初始化列表；对于大多数这些，虚拟值是可以的 */
			fc_final_targets = fc_final_targets_contain_srfs = NIL;
			sort_input_targets = sort_input_targets_contain_srfs = NIL;
			fc_grouping_targets = fc_grouping_targets_contain_srfs = NIL;
			fc_scanjoin_targets = list_make1(fc_scanjoin_target);
			fc_scanjoin_targets_contain_srfs = NIL;
		}

		/* 应用扫描/连接目标。 */
		fc_scanjoin_target_same_exprs = list_length(fc_scanjoin_targets) == 1
			&& equal(fc_scanjoin_target->exprs, fc_current_rel->reltarget->exprs);
		fc_apply_scanjoin_target_to_paths(fc_root, fc_current_rel, fc_scanjoin_targets,
									   fc_scanjoin_targets_contain_srfs,
									   fc_scanjoin_target_parallel_safe,
									   fc_scanjoin_target_same_exprs);

		/*
		 * 将我们刚刚计算出的各个 upper-rel PathTargets 保存到
		 * root->upper_targets[] 中。核心代码不使用这个，但它
		 * 为扩展提供了一个方便的地方以获取信息。出于
		 * 一致性，我们保存所有中间目标，即使某些对应的
		 * upperrels 可能不需要用于此查询。
		 */
		fc_root->upper_targets[UPPERREL_FINAL] = fc_final_target;
		fc_root->upper_targets[UPPERREL_ORDERED] = fc_final_target;
		fc_root->upper_targets[UPPERREL_PARTIAL_DISTINCT] = sort_input_target;
		fc_root->upper_targets[UPPERREL_DISTINCT] = sort_input_target;
		fc_root->upper_targets[UPPERREL_WINDOW] = sort_input_target;
		fc_root->upper_targets[UPPERREL_GROUP_AGG] = fc_grouping_target;

		/*
		 * 如果我们有分组和/或聚合，考虑实现这些的方式。
		 * 我们构建一个新的 upperrel，表示此阶段的输出。
		 */
		if (fc_have_grouping)
		{
			fc_current_rel = fc_create_grouping_paths(fc_root,
												fc_current_rel,
												fc_grouping_target,
												fc_grouping_target_parallel_safe,
												fc_gset_data);
			/* 如果 grouping_target 包含 SRF，请进行修正 */
			if (fc_parse->hasTargetSRFs)
				fc_adjust_paths_for_srfs(fc_root, fc_current_rel,
									  fc_grouping_targets,
									  fc_grouping_targets_contain_srfs);
		}

		/*
		 * 如果我们有窗口函数，考虑实现这些的方式。我们
		 * 构建一个新的 upperrel，表示此阶段的输出。
		 */
		if (fc_activeWindows)
		{
			fc_current_rel = fc_create_window_paths(fc_root,
											  fc_current_rel,
											  fc_grouping_target,
											  sort_input_target,
											  sort_input_target_parallel_safe,
											  fc_wflists,
											  fc_activeWindows);
			/* 如果 sort_input_target 包含 SRF，请进行修正 */
			if (fc_parse->hasTargetSRFs)
				fc_adjust_paths_for_srfs(fc_root, fc_current_rel,
									  sort_input_targets,
									  sort_input_targets_contain_srfs);
		}

		/*
		 * 如果有 DISTINCT 子句，考虑实现该子句的方式。我们
		 * 构建一个新的 upperrel，表示此阶段的输出。
		 */
		if (fc_parse->distinctClause)
		{
			fc_current_rel = fc_create_distinct_paths(fc_root,
												fc_current_rel);
		}
	}							/* if (setOperations) 结束 */

	/*
	 * 如果给定了 ORDER BY，考虑实现该功能，并生成一个
	 * 只包含发出正确排序并投影正确 final_target 的新 upperrel。
	 * 我们可以在此处对排序费用应用原始 limit_tuples 限制，但
	 * 仅当没有推迟的 SRF 时。
	 */
	if (fc_parse->sortClause)
	{
		fc_current_rel = fc_create_ordered_paths(fc_root,
										   fc_current_rel,
										   fc_final_target,
										   fc_final_target_parallel_safe,
										   fc_have_postponed_srfs ? -1.0 :
										   fc_limit_tuples);
		/* 如果 final_target 包含 SRF，请进行修正 */
		if (fc_parse->hasTargetSRFs)
			fc_adjust_paths_for_srfs(fc_root, fc_current_rel,
								  fc_final_targets,
								  fc_final_targets_contain_srfs);
	}

	/*
	 * 现在我们准备构建最终输出的 upperrel。
	 */
	fc_final_rel = fetch_upper_rel(fc_root, UPPERREL_FINAL, NULL);

	/*
	 * 如果输入关系被标记为 consider_parallel 并且 LIMIT 子句中
	 * 没有任何不安全的内容，则 final_rel 也可以标记为
	 * consider_parallel。请注意，如果查询具有 rowMarks
	 * 或不是 SELECT，则查询中的每个关系的 consider_parallel
	 * 将为 false。
	 */
	if (fc_current_rel->consider_parallel &&
		is_parallel_safe(fc_root, fc_parse->limitOffset) &&
		is_parallel_safe(fc_root, fc_parse->limitCount))
		fc_final_rel->consider_parallel = true;

	/*
	 * 如果 current_rel 属于单个 FDW，final_rel 也属于该 FDW。
	 */
	fc_final_rel->serverid = fc_current_rel->serverid;
	fc_final_rel->userid = fc_current_rel->userid;
	fc_final_rel->useridiscurrent = fc_current_rel->useridiscurrent;
	fc_final_rel->fdwroutine = fc_current_rel->fdwroutine;

	/*
	 * 为 final_rel 生成路径。如果需要，插入所有存活的路径，
	 * 并添加 LockRows、Limit 和/或 ModifyTable 步骤。
	 */
	foreach(fc_lc, fc_current_rel->pathlist)
	{
		Path	   *fc_path = (Path *) lfirst(fc_lc);

		/*
		 * 如果有 FOR [KEY] UPDATE/SHARE 子句，添加 LockRows 节点。
		 * （注意：我们故意在这里测试 parse->rowMarks 而不是
		 * root->rowMarks。如果仅有非锁定的 rowmarks，应该
		 * 由 ModifyTable 节点处理。但是，root->rowMarks
		 * 是进入 LockRows 节点的内容。）
		 */
		if (fc_parse->rowMarks)
		{
			fc_path = (Path *) create_lockrows_path(fc_root, fc_final_rel, fc_path,
												 fc_root->rowMarks,
												 assign_special_exec_param(fc_root));
		}

		/*
		 * 如果有 LIMIT/OFFSET 子句，添加 LIMIT 节点。
		 */
		if (limit_needed(fc_parse))
		{
			fc_path = (Path *) create_limit_path(fc_root, fc_final_rel, fc_path,
											  fc_parse->limitOffset,
											  fc_parse->limitCount,
											  fc_parse->limitOption,
											  fc_offset_est, fc_count_est);
		}

		/*
		 * 如果这是 INSERT/UPDATE/DELETE/MERGE，添加 ModifyTable 节点。
		 */
		if (fc_parse->commandType != CMD_SELECT)
		{
			Index		fc_rootRelation;
			List	   *fc_resultRelations = NIL;
			List	   *fc_updateColnosLists = NIL;
			List	   *fc_withCheckOptionLists = NIL;
			List	   *fc_returningLists = NIL;
			List	   *fc_mergeActionLists = NIL;
			List	   *fc_rowMarks;

			if (bms_membership(fc_root->all_result_relids) == BMS_MULTIPLE)
			{
				/* 继承的 UPDATE/DELETE/MERGE */
				RelOptInfo *fc_top_result_rel = find_base_rel(fc_root,
														   fc_parse->resultRelation);
				int			fc_resultRelation = -1;

				/* 将根结果 rel 传递给执行器。 */
				fc_rootRelation = fc_parse->resultRelation;

				/* 只将叶子子节点添加到 ModifyTable。 */
				while ((fc_resultRelation = bms_next_member(fc_root->leaf_result_relids,
														 fc_resultRelation)) >= 0)
				{
					RelOptInfo *fc_this_result_rel = find_base_rel(fc_root,
																fc_resultRelation);

					/*
					 * 还排除自添加到列表以来已变为虚拟的任何叶
					 * rel，例如因被约束排除而被排除。
					 */
					if (IS_DUMMY_REL(fc_this_result_rel))
						continue;

					/* 构建 ModifyTable 需要的每个目标关系列表 */
					fc_resultRelations = lappend_int(fc_resultRelations,
												  fc_resultRelation);
					if (fc_parse->commandType == CMD_UPDATE)
					{
						List	   *update_colnos = fc_root->update_colnos;

						if (fc_this_result_rel != fc_top_result_rel)
							update_colnos =
								adjust_inherited_attnums_multilevel(fc_root,
																	update_colnos,
																	fc_this_result_rel->relid,
																	fc_top_result_rel->relid);
						fc_updateColnosLists = lappend(fc_updateColnosLists,
													update_colnos);
					}
					if (fc_parse->withCheckOptions)
					{
						List	   *fc_withCheckOptions = fc_parse->withCheckOptions;

						if (fc_this_result_rel != fc_top_result_rel)
							fc_withCheckOptions = (List *)
								adjust_appendrel_attrs_multilevel(fc_root,
																  (Node *) fc_withCheckOptions,
																  fc_this_result_rel->relids,
																  fc_top_result_rel->relids);
						fc_withCheckOptionLists = lappend(fc_withCheckOptionLists,
													   fc_withCheckOptions);
					}
					if (fc_parse->returningList)
					{
						List	   *fc_returningList = fc_parse->returningList;

						if (fc_this_result_rel != fc_top_result_rel)
							fc_returningList = (List *)
								adjust_appendrel_attrs_multilevel(fc_root,
																  (Node *) fc_returningList,
																  fc_this_result_rel->relids,
																  fc_top_result_rel->relids);
						fc_returningLists = lappend(fc_returningLists,
												 fc_returningList);
					}
					if (fc_parse->mergeActionList)
					{
						ListCell   *fc_l;
						List	   *fc_mergeActionList = NIL;

						/*
						 * 复制 MergeActions 并翻译引用属性编号的内容。
						 */
						foreach(fc_l, fc_parse->mergeActionList)
						{
							MergeAction *fc_action = lfirst(fc_l),
									   *fc_leaf_action = copyObject(fc_action);

							fc_leaf_action->qual =
								adjust_appendrel_attrs_multilevel(fc_root,
																  (Node *) fc_action->qual,
																  fc_this_result_rel->relids,
																  fc_top_result_rel->relids);
							fc_leaf_action->targetList = (List *)
								adjust_appendrel_attrs_multilevel(fc_root,
																  (Node *) fc_action->targetList,
																  fc_this_result_rel->relids,
																  fc_top_result_rel->relids);
							if (fc_leaf_action->commandType == CMD_UPDATE)
								fc_leaf_action->updateColnos =
									adjust_inherited_attnums_multilevel(fc_root,
																		fc_action->updateColnos,
																		fc_this_result_rel->relid,
																		fc_top_result_rel->relid);
							fc_mergeActionList = lappend(fc_mergeActionList,
													  fc_leaf_action);
						}

						fc_mergeActionLists = lappend(fc_mergeActionLists,
												   fc_mergeActionList);
					}
				}

				if (fc_resultRelations == NIL)
				{
					/*
					 * 我们成功排除了每个子关系，所以使用顶级目标关系的信息生成一个虚拟的单关系计划
					 * （即使那可能不是一个叶子目标）。
					 * 尽管很明显没有数据会被更新或删除，但我们仍然需要有一个 ModifyTable 节点，以便任何语句触发器能够被执行。 
					 * （如果我们修复 nodeModifyTable.c 以允许零目标关系，这可能会更简单，但这在
					 * 网络上未必会有实际好处。）
					 */
					fc_resultRelations = list_make1_int(fc_parse->resultRelation);
					if (fc_parse->commandType == CMD_UPDATE)
						fc_updateColnosLists = list_make1(fc_root->update_colnos);
					if (fc_parse->withCheckOptions)
						fc_withCheckOptionLists = list_make1(fc_parse->withCheckOptions);
					if (fc_parse->returningList)
						fc_returningLists = list_make1(fc_parse->returningList);
					if (fc_parse->mergeActionList)
						fc_mergeActionLists = list_make1(fc_parse->mergeActionList);
				}
			}
			else
			{
				/* 单关系 INSERT/UPDATE/DELETE/MERGE。 */
				fc_rootRelation = 0;	/* 这里没有单独的根关系 */
				fc_resultRelations = list_make1_int(fc_parse->resultRelation);
				if (fc_parse->commandType == CMD_UPDATE)
					fc_updateColnosLists = list_make1(fc_root->update_colnos);
				if (fc_parse->withCheckOptions)
					fc_withCheckOptionLists = list_make1(fc_parse->withCheckOptions);
				if (fc_parse->returningList)
					fc_returningLists = list_make1(fc_parse->returningList);
				if (fc_parse->mergeActionList)
					fc_mergeActionLists = list_make1(fc_parse->mergeActionList);
			}

			/*
			 * 如果有 FOR [KEY] UPDATE/SHARE 子句，LockRows 节点
			 * 会处理提取未锁定的标记行，否则我们
			 * 需要让 ModifyTable 做这些工作。
			 */
			if (fc_parse->rowMarks)
				fc_rowMarks = NIL;
			else
				fc_rowMarks = fc_root->rowMarks;

			fc_path = (Path *)
				create_modifytable_path(fc_root, fc_final_rel,
										fc_path,
										fc_parse->commandType,
										fc_parse->canSetTag,
										fc_parse->resultRelation,
										fc_rootRelation,
										fc_root->partColsUpdated,
										fc_resultRelations,
										fc_updateColnosLists,
										fc_withCheckOptionLists,
										fc_returningLists,
										fc_rowMarks,
										fc_parse->onConflict,
										fc_mergeActionLists,
										assign_special_exec_param(fc_root));
		}

		/* 并将其放入 final_rel */
		add_path(fc_final_rel, fc_path);
	}

	/*
	 * 如果外部查询层可能会使用它们，也要为 final_rel 生成部分路径。
	 */
	if (fc_final_rel->consider_parallel && fc_root->query_level > 1 &&
		!limit_needed(fc_parse))
	{
		Assert(!fc_parse->rowMarks && fc_parse->commandType == CMD_SELECT);
		foreach(fc_lc, fc_current_rel->partial_pathlist)
		{
			Path	   *fc_partial_path = (Path *) lfirst(fc_lc);

			add_partial_path(fc_final_rel, fc_partial_path);
		}
	}

	fc_extra.limit_needed = limit_needed(fc_parse);
	fc_extra.limit_tuples = fc_limit_tuples;
	fc_extra.count_est = fc_count_est;
	fc_extra.offset_est = fc_offset_est;

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_final_rel->fdwroutine &&
		fc_final_rel->fdwroutine->GetForeignUpperPaths)
		fc_final_rel->fdwroutine->GetForeignUpperPaths(fc_root, UPPERREL_FINAL,
													fc_current_rel, fc_final_rel,
													&fc_extra);

	/* 让扩展可能添加一些路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_FINAL,
									fc_current_rel, fc_final_rel, &fc_extra);

	/* 注意：目前我们将 set_cheapest() 的执行留给调用者 */
}

/*
 * 为 groupingSets 子句和相关数据进行预处理。这处理了扩展 grouping sets 的初步步骤，将它们组织成汇总列表，并准备注释，
 * 这些注释稍后将填充上大小估计。
 */
static grouping_sets_data *
fc_preprocess_grouping_sets(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;
	List	   *fc_sets;
	int			fc_maxref = 0;
	ListCell   *fc_lc;
	ListCell   *fc_lc_set;
	grouping_sets_data *fc_gd = palloc0(sizeof(grouping_sets_data));

	fc_parse->groupingSets = expand_grouping_sets(fc_parse->groupingSets, fc_parse->groupDistinct, -1);

	fc_gd->any_hashable = false;
	fc_gd->unhashable_refs = NULL;
	fc_gd->unsortable_refs = NULL;
	fc_gd->unsortable_sets = NIL;

	if (fc_parse->groupClause)
	{
		ListCell   *fc_lc;

		foreach(fc_lc, fc_parse->groupClause)
		{
			SortGroupClause *fc_gc = lfirst_node(SortGroupClause, fc_lc);
			Index		fc_ref = fc_gc->tleSortGroupRef;

			if (fc_ref > fc_maxref)
				fc_maxref = fc_ref;

			if (!fc_gc->hashable)
				fc_gd->unhashable_refs = bms_add_member(fc_gd->unhashable_refs, fc_ref);

			if (!OidIsValid(fc_gc->sortop))
				fc_gd->unsortable_refs = bms_add_member(fc_gd->unsortable_refs, fc_ref);
		}
	}

	/* 为重映射分配工作区数组 */
	fc_gd->tleref_to_colnum_map = (int *) palloc((fc_maxref + 1) * sizeof(int));

	/*
	 * 如果我们有任何无法排序的集合，我们必须在尝试准备汇总之前提取它们。无法排序的集合不会经过
	 * reorder_grouping_sets，因此我们必须在这里应用 GroupingSetData 注释。
	 */
	if (!bms_is_empty(fc_gd->unsortable_refs))
	{
		List	   *fc_sortable_sets = NIL;

		foreach(fc_lc, fc_parse->groupingSets)
		{
			List	   *fc_gset = (List *) lfirst(fc_lc);

			if (bms_overlap_list(fc_gd->unsortable_refs, fc_gset))
			{
				GroupingSetData *fc_gs = makeNode(GroupingSetData);

				fc_gs->set = fc_gset;
				fc_gd->unsortable_sets = lappend(fc_gd->unsortable_sets, fc_gs);

				/*
				 * 我们必须在这里强制要求一个无法排序的集合是可哈希的；
				 * 后续代码假设如此。解析分析只检查
				 * 每个单独的列是否是可哈希或可排序的。
				 *
				 * 注意，传递此测试并不保证我们可以
				 * 生成计划；可能还有其他阻碍因素。
				 */
				if (bms_overlap_list(fc_gd->unhashable_refs, fc_gset))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("could not implement GROUP BY"),
							 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));
			}
			else
				fc_sortable_sets = lappend(fc_sortable_sets, fc_gset);
		}

		if (fc_sortable_sets)
			fc_sets = fc_extract_rollup_sets(fc_sortable_sets);
		else
			fc_sets = NIL;
	}
	else
		fc_sets = fc_extract_rollup_sets(fc_parse->groupingSets);

	foreach(fc_lc_set, fc_sets)
	{
		List	   *fc_current_sets = (List *) lfirst(fc_lc_set);
		RollupData *fc_rollup = makeNode(RollupData);
		GroupingSetData *fc_gs;

		/*
		 * 将当前的 grouping sets 列表重新排序为正确的前缀顺序。 
		 * 如果只需要一次聚合过程，尝试使列表与 ORDER BY 子句匹配；如果需要多次过程，则不必考虑这个。
		 *
		 * 注意，这会将集合从最小成员优先排列为最大成员优先，并应用 GroupingSetData 注释，
		 * 尽管数据将在稍后填充。
		 */
		fc_current_sets = fc_reorder_grouping_sets(fc_current_sets,
											 (list_length(fc_sets) == 1
											  ? fc_parse->sortClause
											  : NIL));

		/*
		 * 获取初始（因此也是最大的）分组集。
		 */
		fc_gs = linitial_node(GroupingSetData, fc_current_sets);

		/*
		 * 适当地排列 groupClause。如果第一个分组集为空，则 groupClause 也必须为空；
		 * 否则，我们必须强制 groupClause 匹配该分组集的顺序。
		 *
		 * （第一个分组集可以为空，即使 parse->groupClause
		 * 不为空，前提是所有非空的分组集都是无法排序的。
		 * 哈希分组集的组条款将在稍后构建。）
		 */
		if (fc_gs->set)
			fc_rollup->groupClause = fc_preprocess_groupclause(fc_root, fc_gs->set);
		else
			fc_rollup->groupClause = NIL;

		/*
		 * 它是可哈希的吗？我们假装空的集合是可哈希的，即使我们实际上会强制它们不被哈希。
		 * 但如果只有空集合，就不必考虑了（因为在这种情况下我们无法哈希
		 * 任何东西）。
		 */
		if (fc_gs->set &&
			!bms_overlap_list(fc_gd->unhashable_refs, fc_gs->set))
		{
			fc_rollup->hashable = true;
			fc_gd->any_hashable = true;
		}

		/*
		 * 现在我们为这组分组集确定了 groupClause 的顺序，我们需要将分组集中的条目
		 * 从 sortgrouprefs 重新映射到 groupClause 的普通索引（0 基础）。
		 * 不过我们会保留原始形式以备后用。
		 */
		fc_rollup->gsets = fc_remap_to_groupclause_idx(fc_rollup->groupClause,
												 fc_current_sets,
												 fc_gd->tleref_to_colnum_map);
		fc_rollup->gsets_data = fc_current_sets;

		fc_gd->rollups = lappend(fc_gd->rollups, fc_rollup);
	}

	if (fc_gd->unsortable_sets)
	{
		/*
		 * 我们尚未为此确定一个 groupclause，但我们需要基于索引的列表
		 * 进行估算。暂时构建基于整个原始 groupclause 的 hash_sets_idx。
		 */
		fc_gd->hash_sets_idx = fc_remap_to_groupclause_idx(fc_parse->groupClause,
													 fc_gd->unsortable_sets,
													 fc_gd->tleref_to_colnum_map);
		fc_gd->any_hashable = true;
	}

	return fc_gd;
}

/*
 * 给定一个 groupclause 和一个 GroupingSetData 列表，返回映射到给定 groupclause 索引的等效集合
 * （不带注释）。
 */
static List * fc_remap_to_groupclause_idx(List *fc_groupClause,
						 List *fc_gsets,
						 int *fc_tleref_to_colnum_map)
{
	int			fc_ref = 0;
	List	   *fc_result = NIL;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_groupClause)
	{
		SortGroupClause *fc_gc = lfirst_node(SortGroupClause, fc_lc);

		fc_tleref_to_colnum_map[fc_gc->tleSortGroupRef] = fc_ref++;
	}

	foreach(fc_lc, fc_gsets)
	{
		List	   *fc_set = NIL;
		ListCell   *fc_lc2;
		GroupingSetData *fc_gs = lfirst_node(GroupingSetData, fc_lc);

		foreach(fc_lc2, fc_gs->set)
		{
			fc_set = lappend_int(fc_set, fc_tleref_to_colnum_map[lfirst_int(fc_lc2)]);
		}

		fc_result = lappend(fc_result, fc_set);
	}

	return fc_result;
}


/*
 * preprocess_rowmarks - 如果需要则设置 PlanRowMarks
 */
static void fc_preprocess_rowmarks(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;
	Bitmapset  *fc_rels;
	List	   *fc_prowmarks;
	ListCell   *fc_l;
	int			fc_i;

	if (fc_parse->rowMarks)
	{
		/*
		 * 如果在分组中出现 FOR [KEY] UPDATE/SHARE，会有麻烦，因为分组使对个别元组
		 * CTID 的引用无效。这在解析时也会进行检查，但由于规则替换、查询提升等，这并不足够。
		 */
		CheckSelectLocking(fc_parse, linitial_node(RowMarkClause,
												fc_parse->rowMarks)->strength);
	}
	else
	{
		/*
		 * 我们只需要对 UPDATE、DELETE、MERGE 或 FOR [KEY]
		 * UPDATE/SHARE 使用行标记。
		 */
		if (fc_parse->commandType != CMD_UPDATE &&
			fc_parse->commandType != CMD_DELETE &&
			fc_parse->commandType != CMD_MERGE)
			return;
	}

	/*
	 * 除了目标外，我们需要为所有基关系设置行标记。我们制作一个所有基关系的位图集，
	 * 然后移除那些我们不需要或标记为 FOR [KEY] UPDATE/SHARE 的项。
	 */
	fc_rels = get_relids_in_jointree((Node *) fc_parse->jointree, false);
	if (fc_parse->resultRelation)
		fc_rels = bms_del_member(fc_rels, fc_parse->resultRelation);

	/*
	 * 将 RowMarkClauses 转换为 PlanRowMark 表示形式。
	 */
	fc_prowmarks = NIL;
	foreach(fc_l, fc_parse->rowMarks)
	{
		RowMarkClause *fc_rc = lfirst_node(RowMarkClause, fc_l);
		RangeTblEntry *fc_rte = rt_fetch(fc_rc->rti, fc_parse->rtable);
		PlanRowMark *fc_newrc;

		
/*
		 * 目前，从语法上讲，无法对更新/删除目标关系应用 FOR UPDATE 等语句。如果将来这变得可能，我们应该从 PlanRowMark 列表中删除目标。
		 */
		Assert(fc_rc->rti != fc_parse->resultRelation);

		/*
		 * 忽略子查询中的 RowMarkClauses；它们并不是真实的表，无法支持真正的锁定。被扁平化到主查询中的子查询应该被完全忽略。任何没有被扁平化的子查询将在下一个循环中获得 ROW_MARK_COPY 项。
		 */
		if (fc_rte->rtekind != RTE_RELATION)
			continue;

		fc_rels = bms_del_member(fc_rels, fc_rc->rti);

		fc_newrc = makeNode(PlanRowMark);
		fc_newrc->rti = fc_newrc->prti = fc_rc->rti;
		fc_newrc->rowmarkId = ++(fc_root->glob->lastRowMarkId);
		fc_newrc->markType = select_rowmark_type(fc_rte, fc_rc->strength);
		fc_newrc->allMarkTypes = (1 << fc_newrc->markType);
		fc_newrc->strength = fc_rc->strength;
		fc_newrc->waitPolicy = fc_rc->waitPolicy;
		fc_newrc->isParent = false;

		fc_prowmarks = lappend(fc_prowmarks, fc_newrc);
	}

	/*
	 * 现在，为任何非目标、非锁定的基本关系添加行标记。
	 */
	fc_i = 0;
	foreach(fc_l, fc_parse->rtable)
	{
		RangeTblEntry *fc_rte = lfirst_node(RangeTblEntry, fc_l);
		PlanRowMark *fc_newrc;

		fc_i++;
		if (!bms_is_member(fc_i, fc_rels))
			continue;

		fc_newrc = makeNode(PlanRowMark);
		fc_newrc->rti = fc_newrc->prti = fc_i;
		fc_newrc->rowmarkId = ++(fc_root->glob->lastRowMarkId);
		fc_newrc->markType = select_rowmark_type(fc_rte, LCS_NONE);
		fc_newrc->allMarkTypes = (1 << fc_newrc->markType);
		fc_newrc->strength = LCS_NONE;
		fc_newrc->waitPolicy = LockWaitBlock;	/* 不重要 */
		fc_newrc->isParent = false;

		fc_prowmarks = lappend(fc_prowmarks, fc_newrc);
	}

	fc_root->rowMarks = fc_prowmarks;
}

/*
 * 选择要用于特定表的 RowMarkType
 */
RowMarkType
select_rowmark_type(RangeTblEntry *fc_rte, LockClauseStrength fc_strength)
{
	if (fc_rte->rtekind != RTE_RELATION)
	{
		/* 如果根本不是表，使用 ROW_MARK_COPY */
		return ROW_MARK_COPY;
	}
	else if (fc_rte->relkind == RELKIND_FOREIGN_TABLE)
	{
		/* 让 FDW 选择行标记类型，如果它想要的话 */
		FdwRoutine *fc_fdwroutine = GetFdwRoutineByRelId(fc_rte->relid);

		if (fc_fdwroutine->GetForeignRowMarkType != NULL)
			return fc_fdwroutine->GetForeignRowMarkType(fc_rte, fc_strength);
		/* 否则，默认使用 ROW_MARK_COPY */
		return ROW_MARK_COPY;
	}
	else
	{
		/* 常规表，应用适当的锁定类型 */
		switch (fc_strength)
		{
			case LCS_NONE:

				/* 
				 * 我们不需要元组锁，只需要重新获取行的能力。
				 */
				return ROW_MARK_REFERENCE;
				break;
			case LCS_FORKEYSHARE:
				return ROW_MARK_KEYSHARE;
				break;
			case LCS_FORSHARE:
				return ROW_MARK_SHARE;
				break;
			case LCS_FORNOKEYUPDATE:
				return ROW_MARK_NOKEYEXCLUSIVE;
				break;
			case LCS_FORUPDATE:
				return ROW_MARK_EXCLUSIVE;
				break;
		}
		elog(ERROR, "unrecognized LockClauseStrength %d", (int) fc_strength);
		return ROW_MARK_EXCLUSIVE;	/* 保持编译器安静 */
	}
}

/* 
 * preprocess_limit - 对 LIMIT 和/或 OFFSET 子句进行预估
 *
 * 我们尝试估计 LIMIT/OFFSET 子句的值，并将结果传回 *count_est 和 *offset_est。 如果相应的子句不存在，则这些变量设置为 0，如果存在但无法估计其值，则设置为 -1。 （“0”约定对于 OFFSET 是合适的，但对于 LIMIT 来说有点不太准确：实际上我们将 LIMIT 0 估计为 LIMIT 1。 但这与规划器通常不估计少于一行的做法是一致的。） 这些值将传递给 create_limit_path，您可以在更改此代码时查看。
 *
 * 返回值是用于规划查询的适当调整的 tuple_fraction。 这种调整不可重写，因为它反映了 grouping_planner() 一定会采取的计划操作，而不是关于上下文的假设。
 */
static double fc_preprocess_limit(PlannerInfo *fc_root, double fc_tuple_fraction,
				 int64 *fc_offset_est, int64 *fc_count_est)
{
	Query	   *fc_parse = fc_root->parse;
	Node	   *fc_est;
	double		fc_limit_fraction;

	/* 除非 LIMIT 或 OFFSET，否则不应调用 */
	Assert(fc_parse->limitCount || fc_parse->limitOffset);

	/* 
	 * 尝试获取子句值。 我们使用 estimate_expression_value 主要是因为它有时可以对 Params 做一些有用的工作。
	 */
	if (fc_parse->limitCount)
	{
		fc_est = estimate_expression_value(fc_root, fc_parse->limitCount);
		if (fc_est && IsA(fc_est, Const))
		{
			if (((Const *) fc_est)->constisnull)
			{
				/* NULL 表示 LIMIT ALL，即没有限制 */
				*fc_count_est = 0; /* 视为不存在 */
			}
			else
			{
				*fc_count_est = DatumGetInt64(((Const *) fc_est)->constvalue);
				if (*fc_count_est <= 0)
					*fc_count_est = 1; /* 至少强制为 1 */
			}
		}
		else
			*fc_count_est = -1;	/* 无法估计 */
	}
	else
		*fc_count_est = 0;			/* 不存在 */

	if (fc_parse->limitOffset)
	{
		fc_est = estimate_expression_value(fc_root, fc_parse->limitOffset);
		if (fc_est && IsA(fc_est, Const))
		{
			if (((Const *) fc_est)->constisnull)
			{
				/* 将 NULL 视为没有偏移量；执行器也会这样处理 */
				*fc_offset_est = 0;	/* 视为不存在 */
			}
			else
			{
				*fc_offset_est = DatumGetInt64(((Const *) fc_est)->constvalue);
				if (*fc_offset_est < 0)
					*fc_offset_est = 0;	/* 视为不存在 */
			}
		}
		else
			*fc_offset_est = -1;	/* 无法估计 */
	}
	else
		*fc_offset_est = 0;		/* 不存在 */

	if (*fc_count_est != 0)
	{
		/* 
		 * LIMIT 子句限制返回的元组的绝对数量。
		 * 然而，如果它不是一个常量 LIMIT，那么我们就必须猜测；缺乏更好的主意，假设计划结果的 10% 是需要的。
		 */
		if (*fc_count_est < 0 || *fc_offset_est < 0)
		{
			/* LIMIT 或 OFFSET 是一个表达式 ... 弃权 ... */
			fc_limit_fraction = 0.10;
		}
		else
		{
			/* LIMIT（加上 OFFSET，如果有的话）是需要的最大元组数 */
			fc_limit_fraction = (double) *fc_count_est + (double) *fc_offset_est;
		}

		/* 
		 * 如果我们从调用者和 LIMIT 都有绝对限制，使用较小的值；同样，如果它们都是分数。如果一个是分数而另一个是绝对值，我们不能轻易确定哪个更小，但我们使用的启发式是绝对值通常会更小。
		 */
		if (fc_tuple_fraction >= 1.0)
		{
			if (fc_limit_fraction >= 1.0)
			{
				/* 两者都为绝对 */
				fc_tuple_fraction = Min(fc_tuple_fraction, fc_limit_fraction);
			}
			else
			{
				/* 调用者为绝对，限制为分数；使用调用者的值 */
			}
		}
		else if (fc_tuple_fraction > 0.0)
		{
			if (fc_limit_fraction >= 1.0)
			{
				/* 调用者为分数，限制为绝对；使用限制 */
				fc_tuple_fraction = fc_limit_fraction;
			}
			else
			{
				/* 两者都是分数 */
				fc_tuple_fraction = Min(fc_tuple_fraction, fc_limit_fraction);
			}
		}
		else
		{
			/* 从调用者那里没有信息，仅使用限制 */
			fc_tuple_fraction = fc_limit_fraction;
		}
	}
	else if (*fc_offset_est != 0 && fc_tuple_fraction > 0.0)
	{
		/* 
		 * 我们有一个 OFFSET，但没有 LIMIT。 这与 LIMIT 情况完全不同：在这里，我们需要增加而不是减少调用者的 tuple_fraction，因为 OFFSET 的作用是导致获取更多的元组，而不是更少。 但这只在我们获得 tuple_fraction > 0 时才重要。
		 *
		 * 和上面一样，使用 10% 如果 OFFSET 存在但无法估计。
		 */
		if (*fc_offset_est < 0)
			fc_limit_fraction = 0.10;
		else
			fc_limit_fraction = (double) *fc_offset_est;

		/* 
		 * 如果我们从调用者和 OFFSET 都有绝对计数，将它们相加；同样，如果它们都是分数。 如果一个是分数而另一个是绝对值，我们想要取更大的值，并且我们启发式地假设这是分数。
		 */
		if (fc_tuple_fraction >= 1.0)
		{
			if (fc_limit_fraction >= 1.0)
			{
				/* 两者都是绝对，所以相加 */
				fc_tuple_fraction += fc_limit_fraction;
			}
			else
			{
				/* 调用者为绝对，限制为分数；使用限制 */
				fc_tuple_fraction = fc_limit_fraction;
			}
		}
		else
		{
			if (fc_limit_fraction >= 1.0)
			{
				/* 调用者为分数，限制为绝对；使用调用者的值 */
			}
			else
			{
				/* 两者都是分数，所以相加 */
				fc_tuple_fraction += fc_limit_fraction;
				if (fc_tuple_fraction >= 1.0)
					fc_tuple_fraction = 0.0;	/* 假设获取所有 */
			}
		}
	}

	return fc_tuple_fraction;
}

/*
 * limit_needed - 我们是否真的需要一个 Limit 计划节点？
 *
 * 如果我们有常量零的 OFFSET 和常量空的 LIMIT，我们可以跳过添加
 * Limit 节点。值得检查这一点，因为 "OFFSET 0" 是一个常见的
 * 优化界限用语。（因为规划器中的其他地方仅检查 parse->limitOffset 是否为 NULL，
 * 它仍然可以作为优化界限 --- 我们只是在抑制不必要的运行时
 * 开销。）
 *
 * 这看起来可能合并到 preprocess_limit 中，但有一个关键的区别：
 * 在这里我们需要 OFFSET/LIMIT 中的硬常量，而在 preprocess_limit 中
 * 考虑估算值就足够了。
 */
bool limit_needed(Query *fc_parse)
{
	Node	   *fc_node;

	fc_node = fc_parse->limitCount;
	if (fc_node)
	{
		if (IsA(fc_node, Const))
		{
			/* NULL 表示 LIMIT ALL，即没有限制 */
			if (!((Const *) fc_node)->constisnull)
				return true;	/* 带有常量值的 LIMIT */
		}
		else
			return true;		/* 非常量 LIMIT */
	}

	fc_node = fc_parse->limitOffset;
	if (fc_node)
	{
		if (IsA(fc_node, Const))
		{
			/* 将 NULL 视为无偏移；执行器也会这样处理 */
			if (!((Const *) fc_node)->constisnull)
			{
				int64		fc_offset = DatumGetInt64(((Const *) fc_node)->constvalue);

				if (fc_offset != 0)
					return true;	/* 带有非零值的 OFFSET */
			}
		}
		else
			return true;		/* 非常量 OFFSET */
	}

	return false;				/* 不需要 Limit 计划节点 */
}


/*
 * remove_useless_groupby_columns
 *		移除 GROUP BY 子句中因对其他 GROUP BY 列的功能依赖而冗余的任何列。
 *
 * 由于某些其他数据库管理系统不允许引用未分组的列，很常见的是发现
 * 所有列都列在 GROUP BY 中，即使列出主键列就足够。删除这些多余的列
 * 可以避免冗余的排序工作，因此值得这样做。
 *
 * Relcache 无效化将确保当 pkey 约束的基础索引被删除时，缓存计划将失效。
 *
 * 目前，我们仅利用 pkey 约束来做到这一点，然而，我们可能
 * 希望在未来进一步扩展，并且还使用具有 NOT NULL 列的唯一约束。
 * 在这种情况下，计划无效化仍然可以工作，因为关系在删除 NOT NULL
 * 约束时会接收到 relcache 无效化。
 */
static void fc_remove_useless_groupby_columns(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;
	Bitmapset **fc_groupbyattnos;
	Bitmapset **fc_surplusvars;
	ListCell   *fc_lc;
	int			fc_relid;

	/* 如果 GROUP BY 项少于两个，则没有机会做任何事情 */
	if (list_length(fc_parse->groupClause) < 2)
		return;

	/* 如果查询有分组集，则不要调整 GROUP BY 子句 */
	if (fc_parse->groupingSets)
		return;

	/*
	 * 扫描 GROUP BY 子句以查找简单 Vars 的 GROUP BY 项。
	 * 用一个 bitmapset 填充 groupbyattnos[k]，包含 RTE k 的
	 * GROUP BY 项的列 attnos。
	 */
	fc_groupbyattnos = (Bitmapset **) palloc0(sizeof(Bitmapset *) *
										   (list_length(fc_parse->rtable) + 1));
	foreach(fc_lc, fc_parse->groupClause)
	{
		SortGroupClause *fc_sgc = lfirst_node(SortGroupClause, fc_lc);
		TargetEntry *fc_tle = get_sortgroupclause_tle(fc_sgc, fc_parse->targetList);
		Var		   *fc_var = (Var *) fc_tle->expr;

		/*
		 * 忽略非 Vars 和来自其他查询级别的 Vars。
		 *
		 * XXX 原则上，如果所有 Vars 都功能依赖于其他 GROUP BY 项，
		 * 也可以删除包含 Vars 的稳定表达式。但不清楚这种情况出现的频率是否足够
		 * 值得烦恼。
		 */
		if (!IsA(fc_var, Var) ||
			fc_var->varlevelsup > 0)
			continue;

		/* 好的，记住我们有这个 Var */
		fc_relid = fc_var->varno;
		Assert(fc_relid <= list_length(fc_parse->rtable));
		fc_groupbyattnos[fc_relid] = bms_add_member(fc_groupbyattnos[fc_relid],
											  fc_var->varattno - FirstLowInvalidHeapAttributeNumber);
	}

	/*
	 * 考虑每个关系，看看是否可以从 GROUP BY 中移除一些 Vars。
	 * 为了简单和速度，我们在单独的过程中执行实际的移除。
	 * 在这里，我们只是用 bitmapset 填充 surplusvars[k]，
	 * 包含 RTE k 的可移除 GROUP BY 项的列 attnos。
	 */
	fc_surplusvars = NULL;			/* 除非需要，否则不分配数组 */
	fc_relid = 0;
	foreach(fc_lc, fc_parse->rtable)
	{
		RangeTblEntry *fc_rte = lfirst_node(RangeTblEntry, fc_lc);
		Bitmapset  *fc_relattnos;
		Bitmapset  *fc_pkattnos;
		Oid			fc_constraintOid;

		fc_relid++;

		/* 只有普通关系可能具有主键约束 */
		if (fc_rte->rtekind != RTE_RELATION)
			continue;

		/*
		 * 我们必须跳过继承的父表，因为某些子关系可能导致重复行。
		 * 然而，在分区表中不会发生这种情况。
		 */
		if (fc_rte->inh && fc_rte->relkind != RELKIND_PARTITIONED_TABLE)
			continue;

		/* 除非该关系在 GROUP BY 中有多个 Vars，否则无事可做 */
		fc_relattnos = fc_groupbyattnos[fc_relid];
		if (bms_membership(fc_relattnos) != BMS_MULTIPLE)
			continue;

		/*
		 * 如果没有合适的（即非推迟的）主键约束，则无法为这个关系移除任何列。
		 */
		fc_pkattnos = get_primary_key_attnos(fc_rte->relid, false, &fc_constraintOid);
		if (fc_pkattnos == NULL)
			continue;

		/*
		 * 如果主键是 relattnos 的一个真子集，则我们有一些可以移除的
		 * GROUP BY 项。
		 */
		if (bms_subset_compare(fc_pkattnos, fc_relattnos) == BMS_SUBSET1)
		{
			/*
			 * 为了容易记住我们是否发现了可以操作的内容，我们直到找到某些内容时
			 * 才分配 surplusvars[] 数组。
			 */
			if (fc_surplusvars == NULL)
				fc_surplusvars = (Bitmapset **) palloc0(sizeof(Bitmapset *) *
													 (list_length(fc_parse->rtable) + 1));

			/* 记住可移除列的 attnos */
			fc_surplusvars[fc_relid] = bms_difference(fc_relattnos, fc_pkattnos);
		}
	}

	/*
	 * 如果我们找到了任何多余的 Vars，构建一个新的 GROUP BY 子句
	 * 而不包含它们。（注意：这可能会留下某些 TLEs 具有未引用的
	 * ressortgroupref 标记，但这并无害。）
	 */
	if (fc_surplusvars != NULL)
	{
		List	   *fc_new_groupby = NIL;

		foreach(fc_lc, fc_parse->groupClause)
		{
			SortGroupClause *fc_sgc = lfirst_node(SortGroupClause, fc_lc);
			TargetEntry *fc_tle = get_sortgroupclause_tle(fc_sgc, fc_parse->targetList);
			Var		   *fc_var = (Var *) fc_tle->expr;

			/*
			 * 新列表必须包括非 Vars、外部 Vars，以及任何未标记为多余的内容。
			 */
			if (!IsA(fc_var, Var) ||
				fc_var->varlevelsup > 0 ||
				!bms_is_member(fc_var->varattno - FirstLowInvalidHeapAttributeNumber,
							   fc_surplusvars[fc_var->varno]))
				fc_new_groupby = lappend(fc_new_groupby, fc_sgc);
		}

		fc_parse->groupClause = fc_new_groupby;
	}
}


/*
 * preprocess_groupclause - 对 GROUP BY 子句进行准备工作
 *
 * 这里的想法是调整 GROUP BY 元素的顺序
 * （这种调整本身在语义上并不重要）以匹配 ORDER BY，
 * 从而允许一次排序操作同时实现 ORDER BY
 * 要求并为实现 GROUP BY 的 Unique 步骤做好准备。
 *
 * 原则上，考虑 GROUP BY 元素的其他排序可能会很有趣，
 * 这可能与其他可能计划的排序顺序相匹配（例如索引扫描），
 * 从而降低成本。但我们对此不予考虑。 哈希分组通常会获胜。
 *
 * 注意：我们不需要对 distinctClause 进行类似处理，
 * 因为解析器已经强制要求它与 ORDER BY 匹配。
 *
 * 对于分组集，项目的顺序被强制与分组集的一致
 * （并且不在分组集中的项目会被跳过）。 
 * 将分组集元素的排序顺序匹配ORDER BY 的工作 
 * 在其他地方完成。
 */
static List * fc_preprocess_groupclause(PlannerInfo *fc_root, List *fc_force)
{
	Query	   *fc_parse = fc_root->parse;
	List	   *fc_new_groupclause = NIL;
	bool		fc_partial_match;
	ListCell   *fc_sl;
	ListCell   *fc_gl;

	/* 对于分组集，我们需要强制排序 */
	if (fc_force)
	{
		foreach(fc_sl, fc_force)
		{
			Index		fc_ref = lfirst_int(fc_sl);
			SortGroupClause *fc_cl = get_sortgroupref_clause(fc_ref, fc_parse->groupClause);

			fc_new_groupclause = lappend(fc_new_groupclause, fc_cl);
		}

		return fc_new_groupclause;
	}

	/* 如果没有 ORDER BY，这里没有有用的工作 */
	if (fc_parse->sortClause == NIL)
		return fc_parse->groupClause;

	/*
	 * 扫描 ORDER BY 子句并构建匹配的 GROUP BY
	 * 项目列表，但仅限于我们可以匹配前缀的部分。
	 *
	 * 此代码假设 sortClause 不包含重复项。
	 */
	foreach(fc_sl, fc_parse->sortClause)
	{
		SortGroupClause *fc_sc = lfirst_node(SortGroupClause, fc_sl);

		foreach(fc_gl, fc_parse->groupClause)
		{
			SortGroupClause *fc_gc = lfirst_node(SortGroupClause, fc_gl);

			if (equal(fc_gc, fc_sc))
			{
				fc_new_groupclause = lappend(fc_new_groupclause, fc_gc);
				break;
			}
		}
		if (fc_gl == NULL)
			break;				/* 没有匹配，因此停止扫描 */
	}

	/* 我们是否匹配了整个 ORDER BY 列表，还是只是部分匹配？ */
	fc_partial_match = (fc_sl != NULL);

	/* 如果没有匹配，没有重排 GROUP BY 的意义 */
	if (fc_new_groupclause == NIL)
		return fc_parse->groupClause;

	/*
	 * 将任何剩余的 GROUP BY 项目添加到新列表中，但仅当我们
	 * 能够完全匹配时。换句话说，我们仅在一个列表是
	 * 另一个的前缀时才对 GROUP BY 列表进行重新排列
	 * --- 否则就没有可能的共同排序。而且，如果有
	 * 任何不可排序的 GROUP BY 项目，则放弃，因为那样就无望了。
	 */
	foreach(fc_gl, fc_parse->groupClause)
	{
		SortGroupClause *fc_gc = lfirst_node(SortGroupClause, fc_gl);

		if (list_member_ptr(fc_new_groupclause, fc_gc))
			continue;			/* 它匹配了一个 ORDER BY 项目 */
		if (fc_partial_match)
			return fc_parse->groupClause;	/* 放弃，不可能有共同的排序 */
		if (!OidIsValid(fc_gc->sortop))
			return fc_parse->groupClause;	/* 放弃，GROUP BY 不能排序 */
		fc_new_groupclause = lappend(fc_new_groupclause, fc_gc);
	}

	/* 成功 --- 安装重新排列的 GROUP BY 列表 */
	Assert(list_length(fc_parse->groupClause) == list_length(fc_new_groupclause));
	return fc_new_groupclause;
}

/*
 * 提取可以通过单个 rollup 类型聚合传递实现的分组集列表。
 * 返回分组集的列表列表。
 *
 * 输入必须按最小集合优先排序。结果的每个子列表
 * 也按最小集合优先排序。
 *
 * 我们希望在这里产生绝对最小的列表数量，以
 * 避免过多的排序。幸运的是，有一个算法可以做到这一点；
 * 将部分有序集分割成链的最小分区问题
 * （这就是我们所需要的，将分组集列表视为通过集合包含
 * 排序的偏序集）可以映射到在二分图上寻找最大
 * 基数匹配的问题，这在多项式时间内是可解的，最坏情况
 * 不超过 O(n^2.5)，通常更好。由于我们的 N 最多为 4096，
 * 我们不需要考虑回退到启发式或近似方法。
 * （在我的普通系统上，即使优化关闭和断言开启，12维立方体的规划时间也不到半秒。）
 */
static List * fc_extract_rollup_sets(List *fc_groupingSets)
{
	int			fc_num_sets_raw = list_length(fc_groupingSets);
	int			fc_num_empty = 0;
	int			fc_num_sets = 0;	/* 不同的集合 */
	int			fc_num_chains = 0;
	List	   *fc_result = NIL;
	List	  **fc_results;
	List	  **fc_orig_sets;
	Bitmapset **set_masks;
	int		   *fc_chains;
	short	  **fc_adjacency;
	short	   *fc_adjacency_buf;
	BipartiteMatchState *fc_state;
	int			fc_i;
	int			fc_j;
	int			fc_j_size;
	ListCell   *fc_lc1 = list_head(fc_groupingSets);
	ListCell   *fc_lc;

	/*
	 * 首先剔除空集合。 算法不强制要求这样做，
	 * 但当前规划器需要在第一个列表中返回所有空集合，
	 * 所以我们在这里将其剔除，然后再添加回来。
	 */
	while (fc_lc1 && lfirst(fc_lc1) == NIL)
	{
		++fc_num_empty;
		fc_lc1 = lnext(fc_groupingSets, fc_lc1);
	}

	/* 如果结果仅为空集合，则现在放弃。 */
	if (!fc_lc1)
		return list_make1(fc_groupingSets);

	/*----------
	 * 我们在这里并不严格需要移除重复集合，但如果不这样做，
	 * 它们往往会分散在结果中，这有点令人困惑
	 * （如果我们决定优化它们，可能会令人恼火）。
	 * 所以我们在这里将它们移除，之后再添加回来。
	 *
	 * 对于每个非重复集合，我们填充以下内容：
	 *
	 * orig_sets[i] = 原始集合列表
	 * set_masks[i] = 用于测试包含的位图集
	 * adjacency[i] = 邻接索引数组 [n, v1, v2, ... vn]
	 *
	 * chains[i] 将是此集合被分配到的结果组。
	 *
	 * 我们从 1 开始索引所有这些，而不是从 0 开始，因为这样
	 * 方便将 0 留给图算法中的 NIL 节点。
	 *----------
	 */
	fc_orig_sets = palloc0((fc_num_sets_raw + 1) * sizeof(List *));
	set_masks = palloc0((fc_num_sets_raw + 1) * sizeof(Bitmapset *));
	fc_adjacency = palloc0((fc_num_sets_raw + 1) * sizeof(short *));
	fc_adjacency_buf = palloc((fc_num_sets_raw + 1) * sizeof(short));

	fc_j_size = 0;
	fc_j = 0;
	fc_i = 1;

	for_each_cell(fc_lc, fc_groupingSets, fc_lc1)
	{
		List	   *fc_candidate = (List *) lfirst(fc_lc);
		Bitmapset  *fc_candidate_set = NULL;
		ListCell   *fc_lc2;
		int			fc_dup_of = 0;

		foreach(fc_lc2, fc_candidate)
		{
			fc_candidate_set = bms_add_member(fc_candidate_set, lfirst_int(fc_lc2));
		}

		/* 只有在与之前的集合相同长度时，才可能是重复的 */
		if (fc_j_size == list_length(fc_candidate))
		{
			int			fc_k;

			for (fc_k = fc_j; fc_k < fc_i; ++fc_k)
			{
				if (bms_equal(set_masks[fc_k], fc_candidate_set))
				{
					fc_dup_of = fc_k;
					break;
				}
			}
		}
		else if (fc_j_size < list_length(fc_candidate))
		{
			fc_j_size = list_length(fc_candidate);
			fc_j = fc_i;
		}

		if (fc_dup_of > 0)
		{
			fc_orig_sets[fc_dup_of] = lappend(fc_orig_sets[fc_dup_of], fc_candidate);
			bms_free(fc_candidate_set);
		}
		else
		{
			int			fc_k;
			int			fc_n_adj = 0;

			fc_orig_sets[fc_i] = list_make1(fc_candidate);
			set_masks[fc_i] = fc_candidate_set;

			/* 填充邻接列表；无需比较同等大小的集合 */

			for (fc_k = fc_j - 1; fc_k > 0; --fc_k)
			{
				if (bms_is_subset(set_masks[fc_k], fc_candidate_set))
					fc_adjacency_buf[++fc_n_adj] = fc_k;
			}

			if (fc_n_adj > 0)
			{
				fc_adjacency_buf[0] = fc_n_adj;
				fc_adjacency[fc_i] = palloc((fc_n_adj + 1) * sizeof(short));
				memcpy(fc_adjacency[fc_i], fc_adjacency_buf, (fc_n_adj + 1) * sizeof(short));
			}
			else
				fc_adjacency[fc_i] = NULL;

			++fc_i;
		}
	}

	fc_num_sets = fc_i - 1;

	/*
	 * 应用图匹配算法进行操作。
	 */
	fc_state = BipartiteMatch(fc_num_sets, fc_num_sets, fc_adjacency);

	/*
	 * 现在，state->pair* 字段拥有我们需要的分配集合到
	 * 链的信息。如果两个集合 (u,v) 属于同一链，
	 * 则 pair_uv[u] = v 或 pair_vu[v] = u （这两个条件都
	 * 会成立，但我们检查这两个是为了便于一次通过操作）。
	 */
	fc_chains = palloc0((fc_num_sets + 1) * sizeof(int));

	for (fc_i = 1; fc_i <= fc_num_sets; ++fc_i)
	{
		int			fc_u = fc_state->pair_vu[fc_i];
		int			fc_v = fc_state->pair_uv[fc_i];

		if (fc_u > 0 && fc_u < fc_i)
			fc_chains[fc_i] = fc_chains[fc_u];
		else if (fc_v > 0 && fc_v < fc_i)
			fc_chains[fc_i] = fc_chains[fc_v];
		else
			fc_chains[fc_i] = ++fc_num_chains;
	}

	/* 构建结果列表。 */
	fc_results = palloc0((fc_num_chains + 1) * sizeof(List *));

	for (fc_i = 1; fc_i <= fc_num_sets; ++fc_i)
	{
		int			fc_c = fc_chains[fc_i];

		Assert(fc_c > 0);

		fc_results[fc_c] = list_concat(fc_results[fc_c], fc_orig_sets[fc_i]);
	}

	/* 将任何空集合推回第一个列表。 */
	while (fc_num_empty-- > 0)
		fc_results[1] = lcons(NIL, fc_results[1]);

	/* 生成结果列表 */
	for (fc_i = 1; fc_i <= fc_num_chains; ++fc_i)
		fc_result = lappend(fc_result, fc_results[fc_i]);

	/*
	 * 释放所有内容。
	 *
	 * （对于小集合来说，这有些过于繁琐，但对于大集合来说，
	 * 我们可能会占用相当数量的内存。）
	 */
	BipartiteMatchFree(fc_state);
	pfree(fc_results);
	pfree(fc_chains);
	for (fc_i = 1; fc_i <= fc_num_sets; ++fc_i)
		if (fc_adjacency[fc_i])
			pfree(fc_adjacency[fc_i]);
	pfree(fc_adjacency);
	pfree(fc_adjacency_buf);
	pfree(fc_orig_sets);
	for (fc_i = 1; fc_i <= fc_num_sets; ++fc_i)
		bms_free(set_masks[fc_i]);
	pfree(set_masks);

	return fc_result;
}

/*
 * 重新排序分组集列表的元素，使其具有正确的前缀关系。
 * 还插入 GroupingSetData 注释。
 *
 * 输入必须按照小集合优先的顺序排列；结果以大集合优先返回。
 * 请注意，结果与输入没有共享的列表子结构，因此调用者可以安全地
 * 在后续修改它。
 *
 * 如果传入排序子句，我们会尽可能遵循其列的顺序，
 * 以最小化添加不必要排序的机会。
 * （我们在这里努力确保 GROUPING SETS ((a,b,c),(c)) 
 * ORDER BY c,b,a 能够在一次传递中实现。）
 */
static List * fc_reorder_grouping_sets(List *fc_groupingsets, List *fc_sortclause)
{
	ListCell   *fc_lc;
	List	   *fc_previous = NIL;
	List	   *fc_result = NIL;

	foreach(fc_lc, fc_groupingsets)
	{
		List	   *fc_candidate = (List *) lfirst(fc_lc);
		List	   *fc_new_elems = list_difference_int(fc_candidate, fc_previous);
		GroupingSetData *fc_gs = makeNode(GroupingSetData);

		while (list_length(fc_sortclause) > list_length(fc_previous) &&
			   list_length(fc_new_elems) > 0)
		{
			SortGroupClause *fc_sc = list_nth(fc_sortclause, list_length(fc_previous));
			int			fc_ref = fc_sc->tleSortGroupRef;

			if (list_member_int(fc_new_elems, fc_ref))
			{
				fc_previous = lappend_int(fc_previous, fc_ref);
				fc_new_elems = list_delete_int(fc_new_elems, fc_ref);
			}
			else
			{
				/* 从排序子句中分岔；放弃它 */
				fc_sortclause = NIL;
				break;
			}
		}

		fc_previous = list_concat(fc_previous, fc_new_elems);

		fc_gs->set = list_copy(fc_previous);
		fc_result = lcons(fc_gs, fc_result);
	}

	list_free(fc_previous);

	return fc_result;
}

/*
 * 在计划生成期间计算 query_pathkeys 和其他 pathkeys
 */
static void fc_standard_qp_callback(PlannerInfo *fc_root, void *fc_extra)
{
	Query	   *fc_parse = fc_root->parse;
	standard_qp_extra *fc_qp_extra = (standard_qp_extra *) fc_extra;
	List	   *fc_tlist = fc_root->processed_tlist;
	List	   *fc_activeWindows = fc_qp_extra->activeWindows;

	/*
	 * 计算代表分组/排序要求的 pathkeys。
	 * sortClause 当然是可排序的，但 GROUP BY 和 DISTINCT 可能不
	 * 可排序，在这种情况下，我们只需将它们的 pathkeys 保持为空。
	 */
	if (fc_qp_extra->groupClause &&
		grouping_is_sortable(fc_qp_extra->groupClause))
		fc_root->group_pathkeys =
			make_pathkeys_for_sortclauses(fc_root,
										  fc_qp_extra->groupClause,
										  fc_tlist);
	else
		fc_root->group_pathkeys = NIL;

	/* 我们只考虑路径键逻辑中的第一个（底部）窗口 */
	if (fc_activeWindows != NIL)
	{
		WindowClause *fc_wc = linitial_node(WindowClause, fc_activeWindows);

		fc_root->window_pathkeys = fc_make_pathkeys_for_window(fc_root,
														 fc_wc,
														 fc_tlist);
	}
	else
		fc_root->window_pathkeys = NIL;

	if (fc_parse->distinctClause &&
		grouping_is_sortable(fc_parse->distinctClause))
		fc_root->distinct_pathkeys =
			make_pathkeys_for_sortclauses(fc_root,
										  fc_parse->distinctClause,
										  fc_tlist);
	else
		fc_root->distinct_pathkeys = NIL;

	fc_root->sort_pathkeys =
		make_pathkeys_for_sortclauses(fc_root,
									  fc_parse->sortClause,
									  fc_tlist);

	
/*
	 * 确定我们是否想从 query_planner 获取排序结果。
	 *
	 * 如果我们有一个可排序的 GROUP BY 子句，那么我们希望得到一个
	 * 正确排序的结果以便分组。否则，如果我们有需要计算的窗口函数，
	 * 我们会尝试对第一个窗口进行排序。再者，如果有一个比 ORDER BY
	 * 子句更严格的可排序 DISTINCT 子句，我们会尝试生成一个足够好
	 * 的排序输出以满足 DISTINCT。再者，如果有一个 ORDER BY 子句，
	 * 我们希望根据 ORDER BY 子句进行排序。
	 *
	 * 注意：如果我们同时有 ORDER BY 和 GROUP BY，而且 ORDER BY 
	 * 是 GROUP BY 的超集，可能会诱使我们请求按照 ORDER BY 排序 ---
	 * 但这可能会导致我们根本无法利用可用的排序顺序。需要更多思考。 
	 * 选择 DISTINCT 和 ORDER BY 之间要简单得多，因为我们知道解析器
	 * 确保了一个是另一个的超集。
	 */
	if (fc_root->group_pathkeys)
		fc_root->query_pathkeys = fc_root->group_pathkeys;
	else if (fc_root->window_pathkeys)
		fc_root->query_pathkeys = fc_root->window_pathkeys;
	else if (list_length(fc_root->distinct_pathkeys) >
			 list_length(fc_root->sort_pathkeys))
		fc_root->query_pathkeys = fc_root->distinct_pathkeys;
	else if (fc_root->sort_pathkeys)
		fc_root->query_pathkeys = fc_root->sort_pathkeys;
	else
		fc_root->query_pathkeys = NIL;
}

/*
 * 估计通过分组子句产生的组数（如果没有分组则为1）
 *
 * path_rows: 扫描/连接步骤生成的输出行数
 * gd: 分组集数据，包括分组集及其子句的列表
 * target_list: 包含分组子句引用的目标列表
 *
 * 如果进行分组集，我们还会为每个集和每个单独的汇总列表注释gsets数据，旨在后续确定某些组合是否可以改为哈希。
 */
static double get_number_of_groups(PlannerInfo *fc_root,
					 double fc_path_rows,
					 grouping_sets_data *fc_gd,
					 List *fc_target_list)
{
	Query	   *fc_parse = fc_root->parse;
	double		fc_dNumGroups;

	if (fc_parse->groupClause)
	{
		List	   *fc_groupExprs;

		if (fc_parse->groupingSets)
		{
			/* 汇总每个分组集的估计值 */
			ListCell   *fc_lc;
			ListCell   *fc_lc2;

			Assert(fc_gd);			/* 保持Coverity满意 */

			fc_dNumGroups = 0;

			foreach(fc_lc, fc_gd->rollups)
			{
				RollupData *fc_rollup = lfirst_node(RollupData, fc_lc);
				ListCell   *fc_lc;

				fc_groupExprs = get_sortgrouplist_exprs(fc_rollup->groupClause,
													 fc_target_list);

				fc_rollup->numGroups = 0.0;

				forboth(fc_lc, fc_rollup->gsets, fc_lc2, fc_rollup->gsets_data)
				{
					List	   *fc_gset = (List *) lfirst(fc_lc);
					GroupingSetData *fc_gs = lfirst_node(GroupingSetData, fc_lc2);
					double		fc_numGroups = estimate_num_groups(fc_root,
																fc_groupExprs,
																fc_path_rows,
																&fc_gset,
																NULL);

					fc_gs->numGroups = fc_numGroups;
					fc_rollup->numGroups += fc_numGroups;
				}

				fc_dNumGroups += fc_rollup->numGroups;
			}

			if (fc_gd->hash_sets_idx)
			{
				ListCell   *fc_lc;

				fc_gd->dNumHashGroups = 0;

				fc_groupExprs = get_sortgrouplist_exprs(fc_parse->groupClause,
													 fc_target_list);

				forboth(fc_lc, fc_gd->hash_sets_idx, fc_lc2, fc_gd->unsortable_sets)
				{
					List	   *fc_gset = (List *) lfirst(fc_lc);
					GroupingSetData *fc_gs = lfirst_node(GroupingSetData, fc_lc2);
					double		fc_numGroups = estimate_num_groups(fc_root,
																fc_groupExprs,
																fc_path_rows,
																&fc_gset,
																NULL);

					fc_gs->numGroups = fc_numGroups;
					fc_gd->dNumHashGroups += fc_numGroups;
				}

				fc_dNumGroups += fc_gd->dNumHashGroups;
			}
		}
		else
		{
			/* 普通GROUP BY */
			fc_groupExprs = get_sortgrouplist_exprs(fc_parse->groupClause,
												 fc_target_list);

			fc_dNumGroups = estimate_num_groups(fc_root, fc_groupExprs, fc_path_rows,
											 NULL, NULL);
		}
	}
	else if (fc_parse->groupingSets)
	{
		/* 空分组集...每个都有一行结果 */
		fc_dNumGroups = list_length(fc_parse->groupingSets);
	}
	else if (fc_parse->hasAggs || fc_root->hasHavingQual)
	{
		/* 普通聚合，一行结果 */
		fc_dNumGroups = 1;
	}
	else
	{
		/* 没有分组 */
		fc_dNumGroups = 1;
	}

	return fc_dNumGroups;
}

/*
 * create_grouping_paths
 *
 * 构建一个新的上游关系，包含分组和/或聚合的路径。
 * 在这个过程中，我们还构建一个部分分组和/或聚合的路径的上游关系。
 * 部分分组和/或聚合路径需要一个FinalizeAggregate节点来完成聚合。目前，
 * 我们构建的唯一部分分组路径也是部分路径；也就是说，它们需要一个Gather，然后是FinalizeAggregate。
 *
 * input_rel: 包含源数据路径
 * target: 计算结果路径的路径目标
 * gd: 分组集数据，包括分组集及其子句的列表
 *
 * 注意：input_rel中的所有路径预期返回make_group_input_target计算的目标。
 */
static RelOptInfo * fc_create_grouping_paths(PlannerInfo *fc_root,
					  RelOptInfo *fc_input_rel,
					  PathTarget *fc_target,
					  bool fc_target_parallel_safe,
					  grouping_sets_data *fc_gd)
{
	Query	   *fc_parse = fc_root->parse;
	RelOptInfo *fc_grouped_rel;
	RelOptInfo *fc_partially_grouped_rel;
	AggClauseCosts fc_agg_costs;

	MemSet(&fc_agg_costs, 0, sizeof(AggClauseCosts));
	get_agg_clause_costs(fc_root, AGGSPLIT_SIMPLE, &fc_agg_costs);

	/*
	 * 创建分组关系以容纳完全聚合的分组和/或聚合路径。
	 */
	fc_grouped_rel = fc_make_grouping_rel(fc_root, fc_input_rel, fc_target,
									fc_target_parallel_safe, fc_parse->havingQual);

	/*
	 * 根据需要创建退化分组的路径或普通分组的路径。
	 */
	if (fc_is_degenerate_grouping(fc_root))
		fc_create_degenerate_grouping_paths(fc_root, fc_input_rel, fc_grouped_rel);
	else
	{
		int			fc_flags = 0;
		GroupPathExtraData fc_extra;

		/*
		 * 确定是否可以执行基于排序的分组实现。 （注意，如果groupClause为空，
		 * grouping_is_sortable()显然为真，并且所有的pathkeys_contained_in()测试也将成功，
		 * 这样我们将考虑所有存活的输入路径。）
		 *
		 * 如果我们有分组集，我们可能能够排序其中一些但不是全部；在这种情况下，只要我们
		 * 必须考虑任何排序输入计划，我们就需要can_sort为真。
		 */
		if ((fc_gd && fc_gd->rollups != NIL)
			|| grouping_is_sortable(fc_parse->groupClause))
			fc_flags |= GROUPING_CAN_USE_SORT;

		/*
		 * 确定我们是否应该考虑基于哈希的分组实现。
		 *
		 * 哈希聚合仅在我们进行分组时适用。如果我们有分组集，某些组可能是可哈希的，但其他组
		 * 可能不是；在这种情况下，只要没有全球性条件阻止我们哈希（因此我们应该考虑带有哈希的计划），
		 * 我们将can_hash设置为true。
		 *
		 * 执行器不支持带有DISTINCT或ORDER BY聚合的哈希聚合。 （这样做会暗示将*所有*输入值
		 * 存储在哈希表中，和/或并行运行多个排序，两者似乎都是一定会失败的。）我们同样不支持
		 * 在哈希聚合中有序集聚合，但该情况也包括在numOrderedAggs计数中。
		 *
		 * 注意：grouping_is_hashable()的检查要比其他门控条件昂贵得多，因此我们希望将其
		 * 放在最后。
		 */
		if ((fc_parse->groupClause != NIL &&
			 fc_root->numOrderedAggs == 0 &&
			 (fc_gd ? fc_gd->any_hashable : grouping_is_hashable(fc_parse->groupClause))))
			fc_flags |= GROUPING_CAN_USE_HASH;

		/*
		 * 确定是否可能进行部分聚合。
		 */
		if (fc_can_partial_agg(fc_root))
			fc_flags |= GROUPING_CAN_PARTIAL_AGG;

		fc_extra.flags = fc_flags;
		fc_extra.target_parallel_safe = fc_target_parallel_safe;
		fc_extra.havingQual = fc_parse->havingQual;
		fc_extra.targetList = fc_parse->targetList;
		fc_extra.partial_costs_set = false;

		/*
		 * 确定理论上是否可以进行分区聚合。
		 * 用户可以禁用此功能，目前，我们不尝试支持分组集。
		 * create_ordinary_grouping_paths()将检查其他条件，例如输入关系是否是分区的。
		 */
		if (enable_partitionwise_aggregate && !fc_parse->groupingSets)
			fc_extra.patype = PARTITIONWISE_AGGREGATE_FULL;
		else
			fc_extra.patype = PARTITIONWISE_AGGREGATE_NONE;

		fc_create_ordinary_grouping_paths(fc_root, fc_input_rel, fc_grouped_rel,
									   &fc_agg_costs, fc_gd, &fc_extra,
									   &fc_partially_grouped_rel);
	}

	set_cheapest(fc_grouped_rel);
	return fc_grouped_rel;
}

/*
 * make_grouping_rel
 *
 * 创建一个新的分组关系并设置基本属性。
 *
 * input_rel表示基础扫描/连接关系。
 * target是从分组关系预期的输出。
 */
static RelOptInfo * fc_make_grouping_rel(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
				  PathTarget *fc_target, bool fc_target_parallel_safe,
				  Node *fc_havingQual)
{
	RelOptInfo *fc_grouped_rel;

	if (IS_OTHER_REL(fc_input_rel))
	{
		fc_grouped_rel = fetch_upper_rel(fc_root, UPPERREL_GROUP_AGG,
									  fc_input_rel->relids);
		fc_grouped_rel->reloptkind = RELOPT_OTHER_UPPER_REL;
	}
	else
	{
		/*
		 * 根据传统，主分组关系的relids集合为NULL。 （这可以更改，但可能需要在其他地方进行调整。）
		 */
		fc_grouped_rel = fetch_upper_rel(fc_root, UPPERREL_GROUP_AGG, NULL);
	}

	/* 设置目标。 */
	fc_grouped_rel->reltarget = fc_target;

	/*
	 * 如果输入关系不是并行安全的，则分组关系也不能并行安全。否则，
	 * 只有当目标列表和HAVING资格是并行安全的情况下，它才是并行安全的。
	 */
	if (fc_input_rel->consider_parallel && fc_target_parallel_safe &&
		is_parallel_safe(fc_root, (Node *) fc_havingQual))
		fc_grouped_rel->consider_parallel = true;

	/*
	 * 如果输入关系属于单个FDW，则分组关系也属于同一FDW。
	 */
	fc_grouped_rel->serverid = fc_input_rel->serverid;
	fc_grouped_rel->userid = fc_input_rel->userid;
	fc_grouped_rel->useridiscurrent = fc_input_rel->useridiscurrent;
	fc_grouped_rel->fdwroutine = fc_input_rel->fdwroutine;

	return fc_grouped_rel;
}

/*
 * is_degenerate_grouping
 *
 * 退化分组是指查询具有HAVING资格和/或分组集，但没有聚合和没有GROUP BY
 * （这意味着分组集都是空的）。
 */
static bool fc_is_degenerate_grouping(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;

	return (fc_root->hasHavingQual || fc_parse->groupingSets) &&
		!fc_parse->hasAggs && fc_parse->groupClause == NIL;
}


/*
 * create_degenerate_grouping_paths
 *
 * 当分组为退化时（请参见 is_degenerate_grouping），我们应该根据 HAVING 是否成功为每个分组集输出零行或一行。此外，HAVING 或目标列表中不能有任何变量，因此实际上我们根本不需要 FROM 表！我们可以直接扔掉目前的计划，生成一个结果节点。这个特殊的边界情况足够不寻常，因此不值得扭曲这个模块的结构，以避免必须先生成早期路径。
 */
static void fc_create_degenerate_grouping_paths(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
								 RelOptInfo *fc_grouped_rel)
{
	Query	   *fc_parse = fc_root->parse;
	int			fc_nrows;
	Path	   *fc_path;

	fc_nrows = list_length(fc_parse->groupingSets);
	if (fc_nrows > 1)
	{
		/*
		 * 似乎没有必要编写代码来构造一个 generate_series
		 * 或 values 扫描以发出多行输出。相反，只需制作 N 个克隆
		 * 并将其附加起来。（如果使用易变的 HAVING 子句，这意味着您
		 * 可能会得到 0 到 N 行输出。随便说说，我认为这是
		 * 预期的。）
		 */
		List	   *fc_paths = NIL;

		while (--fc_nrows >= 0)
		{
			fc_path = (Path *)
				create_group_result_path(fc_root, fc_grouped_rel,
										 fc_grouped_rel->reltarget,
										 (List *) fc_parse->havingQual);
			fc_paths = lappend(fc_paths, fc_path);
		}
		fc_path = (Path *)
			create_append_path(fc_root,
							   fc_grouped_rel,
							   fc_paths,
							   NIL,
							   NIL,
							   NULL,
							   0,
							   false,
							   -1);
	}
	else
	{
		/* 没有分组集，或者只有一个，因此输出一行 */
		fc_path = (Path *)
			create_group_result_path(fc_root, fc_grouped_rel,
									 fc_grouped_rel->reltarget,
									 (List *) fc_parse->havingQual);
	}

	add_path(fc_grouped_rel, fc_path);
}

/*
 * create_ordinary_grouping_paths
 *
 * 为普通（即非退化）情况创建分组路径。
 *
 * 我们需要在同一个函数中考虑已排序和哈希聚合，
 * 因为否则（1）如果两种方法都失败，抛出适当的错误消息会更难，
 * 并且（2）如果排序不可能，我们不应允许哈希表大小
 * 考虑使我们放弃使用哈希。
 *
 * *partially_grouped_rel_p 将被设置为此函数创建的部分分组关系，
 * 如果没有创建，则为 NULL。
 */
static void fc_create_ordinary_grouping_paths(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
							   RelOptInfo *fc_grouped_rel,
							   const AggClauseCosts *fc_agg_costs,
							   grouping_sets_data *fc_gd,
							   GroupPathExtraData *fc_extra,
							   RelOptInfo **fc_partially_grouped_rel_p)
{
	Path	   *fc_cheapest_path = fc_input_rel->cheapest_total_path;
	RelOptInfo *fc_partially_grouped_rel = NULL;
	double		fc_dNumGroups;
	PartitionwiseAggregateType fc_patype = PARTITIONWISE_AGGREGATE_NONE;

	/*
	 * 如果这是最顶层的分组关系，或者如果父关系正在执行某种
	 * 分区聚合，那么我们也可能在此级别上执行分区聚合。
	 * 但是，如果输入关系没有分区，则无法进行分区聚合。
	 */
	if (fc_extra->patype != PARTITIONWISE_AGGREGATE_NONE &&
		IS_PARTITIONED_REL(fc_input_rel))
	{
		/*
		 * 如果这是最顶层的关系，或者如果父关系正在执行
		 * 完全分区聚合，那么只要在此级别的 GROUP BY 子句
		 * 包含所有分区列，并且使用的排序与分区排序匹配，我们就可以
		 * 进行完全的分区聚合。否则，我们最多只能进行部分分区聚合。
		 * 但是如果一般情况下不支持部分聚合，那么我们在进行
		 * 分区聚合时也无法使用它。
		 */
		if (fc_extra->patype == PARTITIONWISE_AGGREGATE_FULL &&
			fc_group_by_has_partkey(fc_input_rel, fc_extra->targetList,
								 fc_root->parse->groupClause))
			fc_patype = PARTITIONWISE_AGGREGATE_FULL;
		else if ((fc_extra->flags & GROUPING_CAN_PARTIAL_AGG) != 0)
			fc_patype = PARTITIONWISE_AGGREGATE_PARTIAL;
		else
			fc_patype = PARTITIONWISE_AGGREGATE_NONE;
	}

	/*
	 * 在生成 grouped_rel 的路径之前，我们首先生成任何可能的
	 * 部分分组路径；这样，后面的代码可以轻松考虑
	 * 并行和非并行的分组方法。
	 */
	if ((fc_extra->flags & GROUPING_CAN_PARTIAL_AGG) != 0)
	{
		bool		fc_force_rel_creation;

		/*
		 * 如果我们在此级别进行分区聚合，强制创建一个
		 * partially_grouped_rel，以便我们可以向其添加分区聚合
		 * 路径。
		 */
		fc_force_rel_creation = (fc_patype == PARTITIONWISE_AGGREGATE_PARTIAL);

		fc_partially_grouped_rel =
			fc_create_partial_grouping_paths(fc_root,
										  fc_grouped_rel,
										  fc_input_rel,
										  fc_gd,
										  fc_extra,
										  fc_force_rel_creation);
	}

	/* 设置输出参数。 */
	*fc_partially_grouped_rel_p = fc_partially_grouped_rel;

	/* 应用分区聚合技术，如果可能的话。 */
	if (fc_patype != PARTITIONWISE_AGGREGATE_NONE)
		fc_create_partitionwise_grouping_paths(fc_root, fc_input_rel, fc_grouped_rel,
											fc_partially_grouped_rel, fc_agg_costs,
											fc_gd, fc_patype, fc_extra);

	/* 如果我们只进行部分聚合，直接返回。 */
	if (fc_extra->patype == PARTITIONWISE_AGGREGATE_PARTIAL)
	{
		Assert(fc_partially_grouped_rel);

		if (fc_partially_grouped_rel->pathlist)
			set_cheapest(fc_partially_grouped_rel);

		return;
	}

	/* 收集任何部分分组的部分路径。 */
	if (fc_partially_grouped_rel && fc_partially_grouped_rel->partial_pathlist)
	{
		fc_gather_grouping_paths(fc_root, fc_partially_grouped_rel);
		set_cheapest(fc_partially_grouped_rel);
	}

	/*
	 * 估计分组数量。
	 */
	fc_dNumGroups = get_number_of_groups(fc_root,
									  fc_cheapest_path->rows,
									  fc_gd,
									  fc_extra->targetList);

	/* 构建最终分组路径 */
	add_paths_to_grouping_rel(fc_root, fc_input_rel, fc_grouped_rel,
							  fc_partially_grouped_rel, fc_agg_costs, fc_gd,
							  fc_dNumGroups, fc_extra);

	/* 如果未能找到任何实现，给出有帮助的错误提示 */
	if (fc_grouped_rel->pathlist == NIL)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("could not implement GROUP BY"),
				 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_grouped_rel->fdwroutine &&
		fc_grouped_rel->fdwroutine->GetForeignUpperPaths)
		fc_grouped_rel->fdwroutine->GetForeignUpperPaths(fc_root, UPPERREL_GROUP_AGG,
													  fc_input_rel, fc_grouped_rel,
													  fc_extra);

	/* 让扩展可能添加一些路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_GROUP_AGG,
									fc_input_rel, fc_grouped_rel,
									fc_extra);
}

/*
 * 对于给定的输入路径，考虑通过哈希和排序的组合对其进行分组集的可能方式。
 * 这可以多次调用，因此重要的是不对输入进行涂改。没有返回结果，
 * 但是生成的任何路径都会添加到 grouped_rel。
 */
static void fc_consider_groupingsets_paths(PlannerInfo *fc_root,
							RelOptInfo *fc_grouped_rel,
							Path *fc_path,
							bool fc_is_sorted,
							bool fc_can_hash,
							grouping_sets_data *fc_gd,
							const AggClauseCosts *fc_agg_costs,
							double fc_dNumGroups)
{
	Query	   *fc_parse = fc_root->parse;
	Size		fc_hash_mem_limit = get_hash_memory_limit();

	/*
	 * 如果我们没有接受到排序输入，则只考虑完全通过哈希实现的计划。
	 *
	 * 如果看起来可以适合 hash_mem，我们可以哈希所有内容。但如果输入实际上是排序的，
	 * 尽管没有被宣传成这样，我们更愿意利用这一点以减少内存使用。
	 *
	 * 如果没有任何可排序的分组集，则忽略 hash_mem 限制，无论如何生成路径，
	 * 否则我们将失败。
	 */
	if (!fc_is_sorted)
	{
		List	   *fc_new_rollups = NIL;
		RollupData *fc_unhashed_rollup = NULL;
		List	   *fc_sets_data;
		List	   *fc_empty_sets_data = NIL;
		List	   *fc_empty_sets = NIL;
		ListCell   *fc_lc;
		ListCell   *fc_l_start = list_head(fc_gd->rollups);
		AggStrategy fc_strat = AGG_HASHED;
		double		fc_hashsize;
		double		fc_exclude_groups = 0.0;

		Assert(fc_can_hash);

		/*
		 * 如果输入碰巧是有用的排序（即使 is_sorted 为假时也可能发生，
		 * 因为这仅意味着我们的调用者已经为我们设置了排序），
		 * 那么通过利用它来节省一些哈希表空间。
		 * 但我们必须留意边界情况：
		 *
		 * 1）如果有任何空的分组集，那么如果所有非空的分组集都是不可排序的，
		 * group_pathkeys 可能为 NIL。在这种情况下，将有一个仅包含空组的汇总，
		 * pathkeys_contained_in 测试 vacuously 为真；这没问题。
		 *
		 * XXX：上述内容依赖于 group_pathkeys 是从第一个 rollup 生成的。
		 * 如果我们增加考虑多个排序顺序以进行分组输入的能力，
		 * 这个假设可能会失败。
		 *
		 * 2）如果没有空集并且只有不可排序的集合，则 rollups 列表将为空
		 * （因此 l_start == NULL），而 group_pathkeys 将为 NIL；
		 * 我们必须确保 vacuously-true 的 pathkeys_contained_in 测试不会导致我们崩溃。
		 */
		if (fc_l_start != NULL &&
			pathkeys_contained_in(fc_root->group_pathkeys, fc_path->pathkeys))
		{
			fc_unhashed_rollup = lfirst_node(RollupData, fc_l_start);
			fc_exclude_groups = fc_unhashed_rollup->numGroups;
			fc_l_start = lnext(fc_gd->rollups, fc_l_start);
		}

		fc_hashsize = estimate_hashagg_tablesize(fc_root,
											  fc_path,
											  fc_agg_costs,
											  fc_dNumGroups - fc_exclude_groups);

		/*
		 * 如果我们只有不可排序的列可用，gd->rollups 是空的。
		 * 在这种情况下覆盖 hash_mem；否则，我们将依赖排序输入
		 * 情况生成可用的混合路径。
		 */
		if (fc_hashsize > fc_hash_mem_limit && fc_gd->rollups)
			return;				/* 不，无法适应 */

		/*
		 * 我们需要将现有的 rollups 列表拆分成单独的分组集
		 * 并为每个集合重新计算 groupClause。
		 */
		fc_sets_data = list_copy(fc_gd->unsortable_sets);

		for_each_cell(fc_lc, fc_gd->rollups, fc_l_start)
		{
			RollupData *fc_rollup = lfirst_node(RollupData, fc_lc);

			/*
			 * 如果我们发现了一个不可哈希的 rollup，
			 * 而它没有被上面的“实际排序”检查跳过，
			 * 我们无法处理；我们需要排序输入（具有不同的排序顺序），
			 * 但我们无法在此处获取。因此退出；我们将从
			 * is_sorted 情况获取有效路径。
			 *
			 * 空分组集的存在并不使 rollup 不可哈希（见 preprocess_grouping_sets），
			 * 我们在下面特别处理这些情况。
			 */
			if (!fc_rollup->hashable)
				return;

			fc_sets_data = list_concat(fc_sets_data, fc_rollup->gsets_data);
		}
		foreach(fc_lc, fc_sets_data)
		{
			GroupingSetData *fc_gs = lfirst_node(GroupingSetData, fc_lc);
			List	   *fc_gset = fc_gs->set;
			RollupData *fc_rollup;

			if (fc_gset == NIL)
			{
				/* 空的分组集无法被哈希。 */
				fc_empty_sets_data = lappend(fc_empty_sets_data, fc_gs);
				fc_empty_sets = lappend(fc_empty_sets, NIL);
			}
			else
			{
				fc_rollup = makeNode(RollupData);

				fc_rollup->groupClause = fc_preprocess_groupclause(fc_root, fc_gset);
				fc_rollup->gsets_data = list_make1(fc_gs);
				fc_rollup->gsets = fc_remap_to_groupclause_idx(fc_rollup->groupClause,
														 fc_rollup->gsets_data,
														 fc_gd->tleref_to_colnum_map);
				fc_rollup->numGroups = fc_gs->numGroups;
				fc_rollup->hashable = true;
				fc_rollup->is_hashed = true;
				fc_new_rollups = lappend(fc_new_rollups, fc_rollup);
			}
		}

		/*
		 * 如果我们没有找到任何非空的可以哈希的内容，则退出。
		 * 我们将从 is_sorted 情况生成路径。
		 */
		if (fc_new_rollups == NIL)
			return;

		/*
		 * 如果存在空的分组集，它们应该在第一个 rollup 中。
		 */
		Assert(!fc_unhashed_rollup || !fc_empty_sets);

		if (fc_unhashed_rollup)
		{
			fc_new_rollups = lappend(fc_new_rollups, fc_unhashed_rollup);
			fc_strat = AGG_MIXED;
		}
		else if (fc_empty_sets)
		{
			RollupData *fc_rollup = makeNode(RollupData);

			fc_rollup->groupClause = NIL;
			fc_rollup->gsets_data = fc_empty_sets_data;
			fc_rollup->gsets = fc_empty_sets;
			fc_rollup->numGroups = list_length(fc_empty_sets);
			fc_rollup->hashable = false;
			fc_rollup->is_hashed = false;
			fc_new_rollups = lappend(fc_new_rollups, fc_rollup);
			fc_strat = AGG_MIXED;
		}

		add_path(fc_grouped_rel, (Path *)
				 create_groupingsets_path(fc_root,
										  fc_grouped_rel,
										  fc_path,
										  (List *) fc_parse->havingQual,
										  fc_strat,
										  fc_new_rollups,
										  fc_agg_costs,
										  fc_dNumGroups));
		return;
	}

	/*
	 * 如果我们有排序输入，但没有可以用的东西，则退出。
	 */
	if (list_length(fc_gd->rollups) == 0)
		return;

	/*
	 * 给定排序输入，我们尝试生成两条路径：一条是排序的，
	 * 另一条是混合排序/哈希的。（我们需要尝试两者，因为
	 * hashagg 可能被禁用，或者某些列可能不可排序。）
	 *
	 * 如果在其他地方有某些障碍（例如有序聚合）意味着我们根本不应考虑哈希，
	 * 则 can_hash 以 false 传入。
	 */
	if (fc_can_hash && fc_gd->any_hashable)
	{
		List	   *fc_rollups = NIL;
		List	   *fc_hash_sets = list_copy(fc_gd->unsortable_sets);
		double		fc_availspace = fc_hash_mem_limit;
		ListCell   *fc_lc;

		/*
		 * 首先考虑我们根本无法排序的组所需的空间。
		 */
		fc_availspace -= estimate_hashagg_tablesize(fc_root,
												 fc_path,
												 fc_agg_costs,
												 fc_gd->dNumHashGroups);

		if (fc_availspace > 0 && list_length(fc_gd->rollups) > 1)
		{
			double		fc_scale;
			int			fc_num_rollups = list_length(fc_gd->rollups);
			int			fc_k_capacity;
			int		   *fc_k_weights = palloc(fc_num_rollups * sizeof(int));
			Bitmapset  *fc_hash_items = NULL;
			int			fc_i;

			
/*
			 * 我们将其视为一个背包问题：背包容量代表 hash_mem，物品重量是实现单个 rollup 所需的哈希表的估计内存使用量，我们实际上应该将成本节省视为物品的价值；然而，目前分配给排序节点的成本并不能很好地反映比较成本，因此我们将所有物品视为等值（我们哈希的每个 rollup 实际上为我们节省了一个排序）。
			 *
			 * 为了使用离散背包，我们需要将值缩放到合理的小界限范围。我们选择允许 5% 的误差幅度；在最坏的情况下，我们最多有 4096 个 rollups，按 5% 的误差幅度需要稍微超过 42MB 的工作空间。（任何希望规划如此复杂查询的人最好具备足够的内存。在更合理的情况下，最多只有几打 rollups，内存使用将是微不足道的。）
			 *
			 * k_capacity 自然是有限的，但我们将缩放值和权重（如下所示）限制在一定范围内，以避免溢出或下溢（或无用地尝试使用小于 1 字节的缩放因子）。
			 */
			fc_scale = Max(fc_availspace / (20.0 * fc_num_rollups), 1.0);
			fc_k_capacity = (int) floor(fc_availspace / fc_scale);

			/*
			 * 我们将第一个 rollup 排除在考虑之外，因为它与输入排序顺序匹配。我们仅将索引 "i" 分配给考虑进行哈希的那些条目；下面的第二个循环必须使用相同的条件。
			 */
			fc_i = 0;
			for_each_from(fc_lc, fc_gd->rollups, 1)
			{
				RollupData *fc_rollup = lfirst_node(RollupData, fc_lc);

				if (fc_rollup->hashable)
				{
					double		fc_sz = estimate_hashagg_tablesize(fc_root,
																fc_path,
																fc_agg_costs,
																fc_rollup->numGroups);

					/*
					 * 如果 sz 非常大，但 hash_mem（因此比例）很小，在这里避免整数溢出。
					 */
					fc_k_weights[fc_i] = (int) Min(floor(fc_sz / fc_scale),
											 fc_k_capacity + 1.0);
					++fc_i;
				}
			}

			/*
			 * 应用背包算法；计算一组物品，以最大化存储的价值（在本例中为节省的排序数量），同时保持总大小（大致）在容量范围内。
			 */
			if (fc_i > 0)
				fc_hash_items = DiscreteKnapsack(fc_k_capacity, fc_i, fc_k_weights, NULL);

			if (!bms_is_empty(fc_hash_items))
			{
				fc_rollups = list_make1(linitial(fc_gd->rollups));

				fc_i = 0;
				for_each_from(fc_lc, fc_gd->rollups, 1)
				{
					RollupData *fc_rollup = lfirst_node(RollupData, fc_lc);

					if (fc_rollup->hashable)
					{
						if (bms_is_member(fc_i, fc_hash_items))
							fc_hash_sets = list_concat(fc_hash_sets,
													fc_rollup->gsets_data);
						else
							fc_rollups = lappend(fc_rollups, fc_rollup);
						++fc_i;
					}
					else
						fc_rollups = lappend(fc_rollups, fc_rollup);
				}
			}
		}

		if (!fc_rollups && fc_hash_sets)
			fc_rollups = list_copy(fc_gd->rollups);

		foreach(fc_lc, fc_hash_sets)
		{
			GroupingSetData *fc_gs = lfirst_node(GroupingSetData, fc_lc);
			RollupData *fc_rollup = makeNode(RollupData);

			Assert(fc_gs->set != NIL);

			fc_rollup->groupClause = fc_preprocess_groupclause(fc_root, fc_gs->set);
			fc_rollup->gsets_data = list_make1(fc_gs);
			fc_rollup->gsets = fc_remap_to_groupclause_idx(fc_rollup->groupClause,
													 fc_rollup->gsets_data,
													 fc_gd->tleref_to_colnum_map);
			fc_rollup->numGroups = fc_gs->numGroups;
			fc_rollup->hashable = true;
			fc_rollup->is_hashed = true;
			fc_rollups = lcons(fc_rollup, fc_rollups);
		}

		if (fc_rollups)
		{
			add_path(fc_grouped_rel, (Path *)
					 create_groupingsets_path(fc_root,
											  fc_grouped_rel,
											  fc_path,
											  (List *) fc_parse->havingQual,
											  AGG_MIXED,
											  fc_rollups,
											  fc_agg_costs,
											  fc_dNumGroups));
		}
	}

	/*
	 * 现在尝试简单的排序情况。
	 */
	if (!fc_gd->unsortable_sets)
		add_path(fc_grouped_rel, (Path *)
				 create_groupingsets_path(fc_root,
										  fc_grouped_rel,
										  fc_path,
										  (List *) fc_parse->havingQual,
										  AGG_SORTED,
										  fc_gd->rollups,
										  fc_agg_costs,
										  fc_dNumGroups));
}

/*
 * create_window_paths
 *
 * 构建一个新的 upperrel，包含用于窗口函数评估的 Paths。
 *
 * input_rel: 包含源数据的 Paths
 * input_target: make_window_input_target 的结果
 * output_target: 顶层 WindowAggPath 应返回的内容
 * wflists: find_window_functions 的结果
 * activeWindows: select_active_windows 的结果
 *
 * 注意：预计 input_rel 中的所有 Paths 都将返回 input_target。
 */
static RelOptInfo * fc_create_window_paths(PlannerInfo *fc_root,
					RelOptInfo *fc_input_rel,
					PathTarget *fc_input_target,
					PathTarget *fc_output_target,
					bool fc_output_target_parallel_safe,
					WindowFuncLists *fc_wflists,
					List *fc_activeWindows)
{
	RelOptInfo *fc_window_rel;
	ListCell   *fc_lc;

	/* 目前在 (WINDOW, NULL) upperrel 中完成所有工作 */
	fc_window_rel = fetch_upper_rel(fc_root, UPPERREL_WINDOW, NULL);

	/*
	 * 如果输入关系不是并行安全的，那么窗口关系也不能是并行安全的。否则，我们需要检查目标列表和活动窗口是否有非并行安全的结构。
	 */
	if (fc_input_rel->consider_parallel && fc_output_target_parallel_safe &&
		is_parallel_safe(fc_root, (Node *) fc_activeWindows))
		fc_window_rel->consider_parallel = true;

	/*
	 * 如果输入关系属于单个 FDW，那么窗口关系也如此。
	 */
	fc_window_rel->serverid = fc_input_rel->serverid;
	fc_window_rel->userid = fc_input_rel->userid;
	fc_window_rel->useridiscurrent = fc_input_rel->useridiscurrent;
	fc_window_rel->fdwroutine = fc_input_rel->fdwroutine;

	/*
	 * 考虑从现有的最便宜总路径（这可能需要排序）开始计算窗口函数，以及任何满足或部分满足 root->window_pathkeys 的现有路径。
	 */
	foreach(fc_lc, fc_input_rel->pathlist)
	{
		Path	   *fc_path = (Path *) lfirst(fc_lc);
		int			fc_presorted_keys;

		if (fc_path == fc_input_rel->cheapest_total_path ||
			pathkeys_count_contained_in(fc_root->window_pathkeys, fc_path->pathkeys,
										&fc_presorted_keys) ||
			fc_presorted_keys > 0)
			fc_create_one_window_path(fc_root,
								   fc_window_rel,
								   fc_path,
								   fc_input_target,
								   fc_output_target,
								   fc_wflists,
								   fc_activeWindows);
	}

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_window_rel->fdwroutine &&
		fc_window_rel->fdwroutine->GetForeignUpperPaths)
		fc_window_rel->fdwroutine->GetForeignUpperPaths(fc_root, UPPERREL_WINDOW,
													 fc_input_rel, fc_window_rel,
													 NULL);

	/* 让扩展可能添加一些路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_WINDOW,
									fc_input_rel, fc_window_rel, NULL);

	/* 现在选择最佳路径 */
	set_cheapest(fc_window_rel);

	return fc_window_rel;
}

/* 
 * 在给定的路径上堆叠窗口函数实现步骤，并将结果添加到 window_rel。
 *
 * window_rel：上层关系以容纳结果 
 * path：用于的输入路径（必须返回 input_target） 
 * input_target：make_window_input_target 的结果 
 * output_target：最上面的 WindowAggPath 应返回的内容 
 * wflists：find_window_functions 的结果 
 * activeWindows：select_active_windows 的结果 
 */
static void fc_create_one_window_path(PlannerInfo *fc_root,
					   RelOptInfo *fc_window_rel,
					   Path *fc_path,
					   PathTarget *fc_input_target,
					   PathTarget *fc_output_target,
					   WindowFuncLists *fc_wflists,
					   List *fc_activeWindows)
{
	PathTarget *fc_window_target;
	ListCell   *l;
	List	   *fc_topqual = NIL;

	/* 
	 * 由于每个窗口子句可能需要不同的排序顺序，我们为每个子句堆叠一个 WindowAgg 节点，并根据需要在它们之间添加排序步骤。
	 * （我们假设 select_active_windows 选择了执行子句的良好顺序。）
	 * 
	 * input_target 应包含生成结果所需的所有 Vars 和 Aggs。
	 * （在某些情况下，我们不需要将所有这些一一传递到顶部，因为它们可能仅用于 WindowFuncs 的输入。
	 * 不过，可能不值得尝试优化这一点。）它还必须包含所有窗口分区和排序表达式，以确保它们仅在堆栈底部计算一次（这对易变函数至关重要）。
	 * 随着我们上升到堆栈的顶部，我们将添加每个级别计算的 WindowFuncs 的输出。
	 */
	fc_window_target = fc_input_target;

	foreach(l, fc_activeWindows)
	{
		WindowClause *fc_wc = lfirst_node(WindowClause, l);
		List	   *fc_window_pathkeys;
		int			fc_presorted_keys;
		bool		fc_is_sorted;
		bool		fc_topwindow;

		fc_window_pathkeys = fc_make_pathkeys_for_window(fc_root,
												   fc_wc,
												   fc_root->processed_tlist);

		fc_is_sorted = pathkeys_count_contained_in(fc_window_pathkeys,
												fc_path->pathkeys,
												&fc_presorted_keys);

		/* 如有必要，请排序 */
		if (!fc_is_sorted)
		{
			/* 
			 * 没有预排序的键或增量排序被禁用，只需执行完整排序。
			 */
			if (fc_presorted_keys == 0 || !enable_incremental_sort)
				fc_path = (Path *) create_sort_path(fc_root, fc_window_rel,
												 fc_path,
												 fc_window_pathkeys,
												 -1.0);
			else
			{
				/* 
				 * 由于我们有预排序的键并且启用了增量排序，只需使用增量排序。
				 */
				fc_path = (Path *) create_incremental_sort_path(fc_root,
															 fc_window_rel,
															 fc_path,
															 fc_window_pathkeys,
															 fc_presorted_keys,
															 -1.0);
			}
		}

		if (lnext(fc_activeWindows, l))
		{
			/* 
			 * 将当前 WindowFuncs 添加到此中间 WindowAggPath 的输出目标。
			 * 我们必须复制 window_target 以避免更改先前路径的目标。
			 *
			 * 注意：WindowFunc 不会增加目标的评估成本；但我们确实需要考虑 tlist 宽度的增加。
			 */
			ListCell   *fc_lc2;

			fc_window_target = copy_pathtarget(fc_window_target);
			foreach(fc_lc2, fc_wflists->windowFuncs[fc_wc->winref])
			{
				WindowFunc *fc_wfunc = lfirst_node(WindowFunc, fc_lc2);

				add_column_to_pathtarget(fc_window_target, (Expr *) fc_wfunc, 0);
				fc_window_target->width += get_typavgwidth(fc_wfunc->wintype, -1);
			}
		}
		else
		{
			/* 在最上面的 WindowAgg 中安装目标目标 */
			fc_window_target = fc_output_target;
		}

		/* 将列表中的最后一个项标记为顶级窗口 */
		fc_topwindow = foreach_current_index(l) == list_length(fc_activeWindows) - 1;

		/* 
		 * 从每个中间 WindowClause 中累计所有运行条件。
		 * 顶层 WindowAgg 必须将这些作为条件传递，以便它正确地过滤掉不需要的元组。
		 */
		if (!fc_topwindow)
			fc_topqual = list_concat(fc_topqual, fc_wc->runCondition);

		fc_path = (Path *)
			create_windowagg_path(fc_root, fc_window_rel, fc_path, fc_window_target,
								  fc_wflists->windowFuncs[fc_wc->winref],
								  fc_wc, fc_topwindow ? fc_topqual : NIL, fc_topwindow);
	}

	add_path(fc_window_rel, fc_path);
}

/* 
 * create_distinct_paths
 *
 * 构建一个新的 upperrel，包含用于 SELECT DISTINCT 评估的路径。
 *
 * input_rel：包含源数据路径
 *
 * 注意：输入路径应该已经计算出所需的 pathtarget，因为 Sort/Unique 不会投影任何内容。
 */
static RelOptInfo * fc_create_distinct_paths(PlannerInfo *fc_root, RelOptInfo *fc_input_rel)
{
	RelOptInfo *fc_distinct_rel;

	/* 目前，在 (DISTINCT, NULL) upperrel 中完成所有工作 */
	fc_distinct_rel = fetch_upper_rel(fc_root, UPPERREL_DISTINCT, NULL);

	/* 
	 * 我们在这一层不计算任何内容，所以 distinct_rel 如果输入关系是并行安全的，则也是并行安全的。
	 * 特别是，如果有 DISTINCT ON (...) 子句，输入关系的任何路径将输出这些表达式，且除非这些表达式是并行安全的，否则不会并行安全。
	 */
	fc_distinct_rel->consider_parallel = fc_input_rel->consider_parallel;

	/* 
	 * 如果 input_rel 属于单个 FDW，distinct_rel 也属于单个 FDW。
	 */
	fc_distinct_rel->serverid = fc_input_rel->serverid;
	fc_distinct_rel->userid = fc_input_rel->userid;
	fc_distinct_rel->useridiscurrent = fc_input_rel->useridiscurrent;
	fc_distinct_rel->fdwroutine = fc_input_rel->fdwroutine;

	/* 基于 input_rel 的 pathlist 构建独特路径 */
	fc_create_final_distinct_paths(fc_root, fc_input_rel, fc_distinct_rel);

	/* 现在基于 input_rel 的 partial_pathlist 构建独特路径 */
	fc_create_partial_distinct_paths(fc_root, fc_input_rel, fc_distinct_rel);

	/* 如果我们未能创建任何路径，则提供一个有用的错误 */
	if (fc_distinct_rel->pathlist == NIL)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("could not implement DISTINCT"),
				 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_distinct_rel->fdwroutine &&
		fc_distinct_rel->fdwroutine->GetForeignUpperPaths)
		fc_distinct_rel->fdwroutine->GetForeignUpperPaths(fc_root,
													   UPPERREL_DISTINCT,
													   fc_input_rel,
													   fc_distinct_rel,
													   NULL);

	/* 让扩展可能添加一些路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_DISTINCT, fc_input_rel,
									fc_distinct_rel, NULL);

	/* 现在选择最佳路径 */
	set_cheapest(fc_distinct_rel);

	return fc_distinct_rel;
}

/* 
 * create_partial_distinct_paths
 *
 * 处理 'input_rel' 部分路径，并将唯一/聚合路径添加到 UPPERREL_PARTIAL_DISTINCT 关系。
 * 对于创建的路径，在顶部添加 Gather/GatherMerge 路径，并添加最终唯一/聚合路径以删除来自并行工作器组合行产生的任何重复项。
 */
static void fc_create_partial_distinct_paths(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
							  RelOptInfo *fc_final_distinct_rel)
{
	RelOptInfo *fc_partial_distinct_rel;
	Query	   *fc_parse;
	List	   *fc_distinctExprs;
	double		fc_numDistinctRows;
	Path	   *fc_cheapest_partial_path;
	ListCell   *fc_lc;

	/* 当输入关系中没有部分路径时，无需执行任何操作 */
	if (!fc_input_rel->consider_parallel || fc_input_rel->partial_pathlist == NIL)
		return;

	fc_parse = fc_root->parse;

	/* 无法进行并行 DISTINCT ON */
	if (fc_parse->hasDistinctOn)
		return;

	fc_partial_distinct_rel = fetch_upper_rel(fc_root, UPPERREL_PARTIAL_DISTINCT,
										   NULL);
	fc_partial_distinct_rel->reltarget = fc_root->upper_targets[UPPERREL_PARTIAL_DISTINCT];
	fc_partial_distinct_rel->consider_parallel = fc_input_rel->consider_parallel;

	/* 
	 * 如果 input_rel 属于单个 FDW，partial_distinct_rel 也属于单个 FDW。
	 */
	fc_partial_distinct_rel->serverid = fc_input_rel->serverid;
	fc_partial_distinct_rel->userid = fc_input_rel->userid;
	fc_partial_distinct_rel->useridiscurrent = fc_input_rel->useridiscurrent;
	fc_partial_distinct_rel->fdwroutine = fc_input_rel->fdwroutine;

	fc_cheapest_partial_path = linitial(fc_input_rel->partial_pathlist);

	fc_distinctExprs = get_sortgrouplist_exprs(fc_parse->distinctClause,
											fc_parse->targetList);

	/* 估计我们将从每个工作者获得多少个唯一行 */
	fc_numDistinctRows = estimate_num_groups(fc_root, fc_distinctExprs,
										  fc_cheapest_partial_path->rows,
										  NULL, NULL);

	/* 首先尝试在已排序路径之上添加唯一路径 */
	if (grouping_is_sortable(fc_parse->distinctClause))
	{
		foreach(fc_lc, fc_input_rel->partial_pathlist)
		{
			Path	   *fc_path = (Path *) lfirst(fc_lc);

			if (pathkeys_contained_in(fc_root->distinct_pathkeys, fc_path->pathkeys))
			{
				add_partial_path(fc_partial_distinct_rel, (Path *)
								 create_upper_unique_path(fc_root,
														  fc_partial_distinct_rel,
														  fc_path,
														  list_length(fc_root->distinct_pathkeys),
														  fc_numDistinctRows));
			}
		}
	}

	/* 
	 * 现在尝试哈希聚合路径，如果已启用并且哈希可能。由于我们不需要确保在这里创建至少一条路径，因此我们将 enable_hashagg 视为一个硬关闭开关，而不是在 create_final_distinct_paths 中的稍软变体。
	 */
	if (enable_hashagg && grouping_is_hashable(fc_parse->distinctClause))
	{
		add_partial_path(fc_partial_distinct_rel, (Path *)
						 create_agg_path(fc_root,
										 fc_partial_distinct_rel,
										 fc_cheapest_partial_path,
										 fc_cheapest_partial_path->pathtarget,
										 AGG_HASHED,
										 AGGSPLIT_SIMPLE,
										 fc_parse->distinctClause,
										 NIL,
										 NULL,
										 fc_numDistinctRows));
	}

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_partial_distinct_rel->fdwroutine &&
		fc_partial_distinct_rel->fdwroutine->GetForeignUpperPaths)
		fc_partial_distinct_rel->fdwroutine->GetForeignUpperPaths(fc_root,
															   UPPERREL_PARTIAL_DISTINCT,
															   fc_input_rel,
															   fc_partial_distinct_rel,
															   NULL);

	/* 让扩展可能添加更多的部分路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_PARTIAL_DISTINCT,
									fc_input_rel, fc_partial_distinct_rel, NULL);

	if (fc_partial_distinct_rel->partial_pathlist != NIL)
	{
		generate_gather_paths(fc_root, fc_partial_distinct_rel, true);
		set_cheapest(fc_partial_distinct_rel);

		/* 
		 * 最后，创建路径以使最终结果唯一化。此步骤是去除由于结合来自并行工作者的行而产生的任何重复项所必需的。
		 */
		fc_create_final_distinct_paths(fc_root, fc_partial_distinct_rel,
									fc_final_distinct_rel);
	}
}

/*
 * create_final_distinct_paths
 *		基于 'input_rel' 路径列表在 'distinct_rel' 中创建不同的路径
 *
 * input_rel：包含源数据路径
 * distinct_rel：用于存储创建的路径的目标关系
 */
static RelOptInfo * fc_create_final_distinct_paths(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
							RelOptInfo *fc_distinct_rel)
{
	Query	   *fc_parse = fc_root->parse;
	Path	   *fc_cheapest_input_path = fc_input_rel->cheapest_total_path;
	double		fc_numDistinctRows;
	bool		fc_allow_hash;
	Path	   *fc_path;
	ListCell   *fc_lc;

	/* 估算将会有多少个不同的行 */
	if (fc_parse->groupClause || fc_parse->groupingSets || fc_parse->hasAggs ||
		fc_root->hasHavingQual)
	{
		/* 
		 * 如果进行了分组或聚合，则使用输入行数作为
		 * 估算的不同行数（即，假设输入已经大部分是唯一的）。
		 */
		fc_numDistinctRows = fc_cheapest_input_path->rows;
	}
	else
	{
		/* 
		 * 否则，UNIQUE 过滤器的效果可与 GROUP BY 相媲美。
		 */
		List	   *fc_distinctExprs;

		fc_distinctExprs = get_sortgrouplist_exprs(fc_parse->distinctClause,
												fc_parse->targetList);
		fc_numDistinctRows = estimate_num_groups(fc_root, fc_distinctExprs,
											  fc_cheapest_input_path->rows,
											  NULL, NULL);
	}

	/* 
	 * 如果可能，考虑基于排序的 DISTINCT 实现。
	 */
	if (grouping_is_sortable(fc_parse->distinctClause))
	{
		/* 
		 * 首先，如果我们有足够预排序的路径，就将一个
		 * Unique 节点附加到这些路径上。然后考虑对
		 * 成本最低的输入路径进行显式排序并进行 Unique 操作。
		 *
		 * 当我们有 DISTINCT ON 时，必须按照 DISTINCT 和
		 * ORDER BY 中更严格的一个进行排序，否则将不会有期望的行为。
		 * 此外，如果必须进行显式排序，我们可以使用
		 * 更严格的排序来避免后续的第二次排序。 （注意
		 * 解析器将确保一个子句是另一个的前缀。）
		 */
		List	   *fc_needed_pathkeys;

		if (fc_parse->hasDistinctOn &&
			list_length(fc_root->distinct_pathkeys) <
			list_length(fc_root->sort_pathkeys))
			fc_needed_pathkeys = fc_root->sort_pathkeys;
		else
			fc_needed_pathkeys = fc_root->distinct_pathkeys;

		foreach(fc_lc, fc_input_rel->pathlist)
		{
			Path	   *fc_path = (Path *) lfirst(fc_lc);

			if (pathkeys_contained_in(fc_needed_pathkeys, fc_path->pathkeys))
			{
				add_path(fc_distinct_rel, (Path *)
						 create_upper_unique_path(fc_root, fc_distinct_rel,
												  fc_path,
												  list_length(fc_root->distinct_pathkeys),
												  fc_numDistinctRows));
			}
		}

		/* 对于显式排序的情况，总是使用更严格的子句 */
		if (list_length(fc_root->distinct_pathkeys) <
			list_length(fc_root->sort_pathkeys))
		{
			fc_needed_pathkeys = fc_root->sort_pathkeys;
			/* 断言检查确保解析器没有搞错... */
			Assert(pathkeys_contained_in(fc_root->distinct_pathkeys,
										 fc_needed_pathkeys));
		}
		else
			fc_needed_pathkeys = fc_root->distinct_pathkeys;

		fc_path = fc_cheapest_input_path;
		if (!pathkeys_contained_in(fc_needed_pathkeys, fc_path->pathkeys))
			fc_path = (Path *) create_sort_path(fc_root, fc_distinct_rel,
											 fc_path,
											 fc_needed_pathkeys,
											 -1.0);

		add_path(fc_distinct_rel, (Path *)
				 create_upper_unique_path(fc_root, fc_distinct_rel,
										  fc_path,
										  list_length(fc_root->distinct_pathkeys),
										  fc_numDistinctRows));
	}

	/* 
	 * 如果可能，考虑基于哈希的 DISTINCT 实现。
	 *
	 * 如果我们无法创建其他类型的路径，我们 *必须* 哈希或
	 * 死亡尝试。如果我们确实有其他选择，有两件事应该
	 * 防止选择哈希：如果查询使用 DISTINCT ON
	 * （因为如果我们哈希将不会真正获得预期的行为），或者
	 * enable_hashagg 关闭。
	 *
	 * 注意：grouping_is_hashable() 的检查成本比
	 * 其他门控条件高得多，因此我们希望最后进行。
	 */
	if (fc_distinct_rel->pathlist == NIL)
		fc_allow_hash = true;		/* 我们没有替代方案 */
	else if (fc_parse->hasDistinctOn || !enable_hashagg)
		fc_allow_hash = false;		/* 基于策略决定不进行哈希 */
	else
		fc_allow_hash = true;		/* 默认 */

	if (fc_allow_hash && grouping_is_hashable(fc_parse->distinctClause))
	{
		/* 生成哈希聚合路径 --- 无需排序 */
		add_path(fc_distinct_rel, (Path *)
				 create_agg_path(fc_root,
								 fc_distinct_rel,
								 fc_cheapest_input_path,
								 fc_cheapest_input_path->pathtarget,
								 AGG_HASHED,
								 AGGSPLIT_SIMPLE,
								 fc_parse->distinctClause,
								 NIL,
								 NULL,
								 fc_numDistinctRows));
	}

	return fc_distinct_rel;
}

/* 
 * create_ordered_paths
 *
 * 构建一个新的 upperrel，包含用于 ORDER BY 评估的路径。
 *
 * 结果中的所有路径必须满足 ORDER BY 排序。
 * 我们需要考虑的唯一新路径是显式完全排序
 * 和对成本最低的现有路径进行增量排序。
 *
 * input_rel：包含源数据路径
 * target：结果路径必须发出的输出 tlist
 * limit_tuples：对输出元组数量的估计限制，
 *		或 -1 如果没有 LIMIT 或无法估计
 *
 * XXX 这只是查看 sort_pathkeys。我想知道它是否需要查看
 * 其他 pathkeys（分组等），就像 generate_useful_gather_paths 一样。
 */
static RelOptInfo * fc_create_ordered_paths(PlannerInfo *fc_root,
					 RelOptInfo *fc_input_rel,
					 PathTarget *fc_target,
					 bool fc_target_parallel_safe,
					 double fc_limit_tuples)
{
	Path	   *fc_cheapest_input_path = fc_input_rel->cheapest_total_path;
	RelOptInfo *fc_ordered_rel;
	ListCell   *fc_lc;

	/* 暂时在（ORDERED，NULL）upperrel 中完成所有工作 */
	fc_ordered_rel = fetch_upper_rel(fc_root, UPPERREL_ORDERED, NULL);

	/* 
	 * 如果输入关系不是并行安全的，那么有序关系
	 * 也不能是并行安全的。否则，如果目标列表是
	 * 并行安全的，则它是并行安全的。
	 */
	if (fc_input_rel->consider_parallel && fc_target_parallel_safe)
		fc_ordered_rel->consider_parallel = true;

	/* 
	 * 如果输入关系属于一个 FDW，则 ordered_rel 也属于
	 * 同一个 FDW。
	 */
	fc_ordered_rel->serverid = fc_input_rel->serverid;
	fc_ordered_rel->userid = fc_input_rel->userid;
	fc_ordered_rel->useridiscurrent = fc_input_rel->useridiscurrent;
	fc_ordered_rel->fdwroutine = fc_input_rel->fdwroutine;

	foreach(fc_lc, fc_input_rel->pathlist)
	{
		Path	   *fc_input_path = (Path *) lfirst(fc_lc);
		Path	   *fc_sorted_path = fc_input_path;
		bool		fc_is_sorted;
		int			fc_presorted_keys;

		fc_is_sorted = pathkeys_count_contained_in(fc_root->sort_pathkeys,
												fc_input_path->pathkeys, &fc_presorted_keys);

		if (fc_is_sorted)
		{
			/* 直接使用输入路径，但如有必要添加投影步骤 */
			if (fc_sorted_path->pathtarget != fc_target)
				fc_sorted_path = apply_projection_to_path(fc_root, fc_ordered_rel,
													   fc_sorted_path, fc_target);

			add_path(fc_ordered_rel, fc_sorted_path);
		}
		else
		{
			/* 
			 * 尝试添加显式排序，但仅适用于成本最低的
			 * 路径，因为完全排序通常会给所有路径增加相同的成本。
			 */
			if (fc_input_path == fc_cheapest_input_path)
			{
				/* 
				 * 对成本最低的输入路径进行排序。这里的显式排序可以
				 * 利用 LIMIT。
				 */
				fc_sorted_path = (Path *) create_sort_path(fc_root,
														fc_ordered_rel,
														fc_input_path,
														fc_root->sort_pathkeys,
														fc_limit_tuples);
				/* 如果需要，添加投影步骤 */
				if (fc_sorted_path->pathtarget != fc_target)
					fc_sorted_path = apply_projection_to_path(fc_root, fc_ordered_rel,
														   fc_sorted_path, fc_target);

				add_path(fc_ordered_rel, fc_sorted_path);
			}

			/* 
			 * 如果启用了增量排序，则也尝试之。与常规排序不同，
			 * 我们不能仅仅查看成本最低的路径，因为增量排序的
			 * 成本取决于路径的预排序程度。此外，增量排序可能
			 * 使较便宜的启动路径胜出，尽管总成本较高。
			 */
			if (!enable_incremental_sort)
				continue;

			/* 同样，如果路径不能用于增量排序。 */
			if (!fc_presorted_keys)
				continue;

			/* 同样考虑增量排序。 */
			fc_sorted_path = (Path *) create_incremental_sort_path(fc_root,
																fc_ordered_rel,
																fc_input_path,
																fc_root->sort_pathkeys,
																fc_presorted_keys,
																fc_limit_tuples);

			/* 如果需要，添加投影步骤 */
			if (fc_sorted_path->pathtarget != fc_target)
				fc_sorted_path = apply_projection_to_path(fc_root, fc_ordered_rel,
													   fc_sorted_path, fc_target);

			add_path(fc_ordered_rel, fc_sorted_path);
		}
	}

	/* 
	 * generate_gather_paths() 将已经为最佳并行路径生成一个简单的 Gather
	 * 路径（如果有），上面的循环将考虑对其进行排序。类似地，generate_gather_paths() 也将
	 * 生成可以在没有排序的情况下使用的顺序保持 Gather Merge 计划，
	 * 如果它们恰好匹配 sort_pathkeys，上面的循环也会处理这些。
	 * 然而，还有一个可能性：根据所需的输出顺序对成本最低的部分路径进行排序，
	 * 然后使用 Gather Merge 可能是有意义的。
	 */
	if (fc_ordered_rel->consider_parallel && fc_root->sort_pathkeys != NIL &&
		fc_input_rel->partial_pathlist != NIL)
	{
		Path	   *fc_cheapest_partial_path;

		fc_cheapest_partial_path = linitial(fc_input_rel->partial_pathlist);

		/* 
		 * 如果成本最低的部分路径不需要排序，这与已尝试的内容是多余的。
		 */
		if (!pathkeys_contained_in(fc_root->sort_pathkeys,
								   fc_cheapest_partial_path->pathkeys))
		{
			Path	   *fc_path;
			double		fc_total_groups;

			fc_path = (Path *) create_sort_path(fc_root,
											 fc_ordered_rel,
											 fc_cheapest_partial_path,
											 fc_root->sort_pathkeys,
											 fc_limit_tuples);

			fc_total_groups = fc_cheapest_partial_path->rows *
				fc_cheapest_partial_path->parallel_workers;
			fc_path = (Path *)
				create_gather_merge_path(fc_root, fc_ordered_rel,
										 fc_path,
										 fc_path->pathtarget,
										 fc_root->sort_pathkeys, NULL,
										 &fc_total_groups);

			/* 如果需要，添加投影步骤 */
			if (fc_path->pathtarget != fc_target)
				fc_path = apply_projection_to_path(fc_root, fc_ordered_rel,
												fc_path, fc_target);

			add_path(fc_ordered_rel, fc_path);
		}

		/* 
		 * 考虑对部分路径进行增量排序并使用聚合合并。
		 *
		 * 当我们只有一个单项 sort_pathkeys 时，我们也可以跳过整个循环，
		 * 因为在没有完全排序的情况下，我们不可能有列表的预排序前缀。
		 */
		if (enable_incremental_sort && list_length(fc_root->sort_pathkeys) > 1)
		{
			ListCell   *fc_lc;

			foreach(fc_lc, fc_input_rel->partial_pathlist)
			{
				Path	   *fc_input_path = (Path *) lfirst(fc_lc);
				Path	   *fc_sorted_path;
				bool		fc_is_sorted;
				int			fc_presorted_keys;
				double		fc_total_groups;

				
/*
				 * 我们不关心这是否是最便宜的部分路径 - 我们
				 * 不能简单地跳过它，因为它可能是部分排序的
				 * 在这种情况下，我们希望考虑添加增量排序
				 * （而不是完全排序，这在上面发生的情况）。
				 */

				fc_is_sorted = pathkeys_count_contained_in(fc_root->sort_pathkeys,
														fc_input_path->pathkeys,
														&fc_presorted_keys);

				/* 在完全排序的路径上添加增量排序没有意义。 */
				if (fc_is_sorted)
					continue;

				if (fc_presorted_keys == 0)
					continue;

				/* 由于我们有预排序的键，考虑增量排序。 */
				fc_sorted_path = (Path *) create_incremental_sort_path(fc_root,
																	fc_ordered_rel,
																	fc_input_path,
																	fc_root->sort_pathkeys,
																	fc_presorted_keys,
																	fc_limit_tuples);
				fc_total_groups = fc_input_path->rows *
					fc_input_path->parallel_workers;
				fc_sorted_path = (Path *)
					create_gather_merge_path(fc_root, fc_ordered_rel,
											 fc_sorted_path,
											 fc_sorted_path->pathtarget,
											 fc_root->sort_pathkeys, NULL,
											 &fc_total_groups);

				/* 如果需要，添加投影步骤 */
				if (fc_sorted_path->pathtarget != fc_target)
					fc_sorted_path = apply_projection_to_path(fc_root, fc_ordered_rel,
														   fc_sorted_path, fc_target);

				add_path(fc_ordered_rel, fc_sorted_path);
			}
		}
	}

	/*
	 * 如果有负责查询所有基关系的 FDW，让它考虑添加 ForeignPaths。
	 */
	if (fc_ordered_rel->fdwroutine &&
		fc_ordered_rel->fdwroutine->GetForeignUpperPaths)
		fc_ordered_rel->fdwroutine->GetForeignUpperPaths(fc_root, UPPERREL_ORDERED,
													  fc_input_rel, fc_ordered_rel,
													  NULL);

	/* 让扩展可能添加一些路径 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_ORDERED,
									fc_input_rel, fc_ordered_rel, NULL);

	/*
	 * 无需在这里调用 set_cheapest；grouping_planner 不需要我们这样做。
	 */
	Assert(fc_ordered_rel->pathlist != NIL);

	return fc_ordered_rel;
}


/*
 * make_group_input_target
 *	  生成适用于分组节点初始输入的 PathTarget。
 *
 * 如果存在分组或聚合，扫描/连接子计划无法发出查询的最终目标列表；例如，肯定无法发出任何聚合函数调用。这个例程生成扫描/连接子计划的正确目标。
 *
 * 从解析器传递的查询目标列表已包含所有 ORDER BY 和 GROUP BY 表达式的条目，但它不会包含仅在 HAVING 子句中使用的变量的条目；因此，我们需要将这些变量添加到子计划目标列表中。此外，我们将所有表达式扁平化，除了 GROUP BY 项目外，其他表达式将在上层计划节点而不是子计划中计算。例如，给定一个查询如下：
 *		SELECT a+b,SUM(c+d) FROM table GROUP BY a+b;
 * 我们希望将此目标列表传递给子计划：
 *		a+b,c,d
 * 其中 a+b 目标将用于排序/分组步骤，而其他目标将用于计算最终结果。
 *
 * 'final_target' 是查询的最终目标列表（以 PathTarget 形式）。
 *
 * 结果是要由 query_planner() 返回的路径计算的 PathTarget。
 */
static PathTarget * fc_make_group_input_target(PlannerInfo *fc_root, PathTarget *fc_final_target)
{
	Query	   *fc_parse = fc_root->parse;
	PathTarget *fc_input_target;
	List	   *fc_non_group_cols;
	List	   *fc_non_group_vars;
	int			fc_i;
	ListCell   *fc_lc;

	/*
	 * 我们必须构建一个包含所有分组列的目标，以及查询目标列表和 HAVING 条件中提到的任何其他变量。
	 */
	fc_input_target = create_empty_pathtarget();
	fc_non_group_cols = NIL;

	fc_i = 0;
	foreach(fc_lc, fc_final_target->exprs)
	{
		Expr	   *fc_expr = (Expr *) lfirst(fc_lc);
		Index		fc_sgref = get_pathtarget_sortgroupref(fc_final_target, fc_i);

		if (fc_sgref && fc_parse->groupClause &&
			get_sortgroupref_clause_noerr(fc_sgref, fc_parse->groupClause) != NULL)
		{
			/*
			 * 它是一个分组列，因此按原样将其添加到输入目标中。
			 */
			add_column_to_pathtarget(fc_input_target, fc_expr, fc_sgref);
		}
		else
		{
			/*
			 * 非分组列，因此只需记住该表达式以便稍后调用 pull_var_clause。
			 */
			fc_non_group_cols = lappend(fc_non_group_cols, fc_expr);
		}

		fc_i++;
	}

	/*
	 * 如果有 HAVING 条件，我们也需要它使用的变量。
	 */
	if (fc_parse->havingQual)
		fc_non_group_cols = lappend(fc_non_group_cols, fc_parse->havingQual);

	/*
	 * 提取所有提到非分组列（加上 HAVING）中的变量，并将它们添加到输入目标中（如果尚未存在）。（直接用作 GROUP BY 项的变量将已经存在。）注意这包括用于 resjunk 项的变量，因此我们满足了 ORDER BY 和窗口规格的需求。在这里也将提取在 Aggrefs 和 WindowFuncs 中使用的变量。
	 */
	fc_non_group_vars = pull_var_clause((Node *) fc_non_group_cols,
									 PVC_RECURSE_AGGREGATES |
									 PVC_RECURSE_WINDOWFUNCS |
									 PVC_INCLUDE_PLACEHOLDERS);
	add_new_columns_to_pathtarget(fc_input_target, fc_non_group_vars);

	/* 清理杂项 */
	list_free(fc_non_group_vars);
	list_free(fc_non_group_cols);

	/* XXX 这导致一些冗余的成本计算…… */
	return set_pathtarget_cost_width(fc_root, fc_input_target);
}

/*
 * make_partial_grouping_target
 *	  生成适用于部分聚合（或部分分组，如果没有聚合）节点输出的 PathTarget。
 *
 * 一个部分聚合节点需要发出与常规聚合节点相同的所有聚合，此外还包括在 HAVING 中使用的任何聚合；除非 Aggref 节点应标记为部分聚合。
 *
 * 此外，我们最好发出在 Aggrefs 外部使用的任何变量和 PlaceHolderVars。在聚合 tlist 和 HAVING 中（大概，这些将是按组或用于分组表达式的变量）。
 *
 * grouping_target 是顶层聚合步骤要发出的 tlist。havingQual 代表 HAVING 子句。
 */
static PathTarget * fc_make_partial_grouping_target(PlannerInfo *fc_root,
							 PathTarget *fc_grouping_target,
							 Node *fc_havingQual)
{
	Query	   *fc_parse = fc_root->parse;
	PathTarget *fc_partial_target;
	List	   *fc_non_group_cols;
	List	   *fc_non_group_exprs;
	int			fc_i;
	ListCell   *fc_lc;

	fc_partial_target = create_empty_pathtarget();
	fc_non_group_cols = NIL;

	fc_i = 0;
	foreach(fc_lc, fc_grouping_target->exprs)
	{
		Expr	   *fc_expr = (Expr *) lfirst(fc_lc);
		Index		fc_sgref = get_pathtarget_sortgroupref(fc_grouping_target, fc_i);

		if (fc_sgref && fc_parse->groupClause &&
			get_sortgroupref_clause_noerr(fc_sgref, fc_parse->groupClause) != NULL)
		{
			/*
			 * 它是一个分组列，因此按原样将其添加到 partial_target 中。
			 * （这允许上层聚合步骤重复分组计算。）
			 */
			add_column_to_pathtarget(fc_partial_target, fc_expr, fc_sgref);
		}
		else
		{
			/*
			 * 非分组列，因此只需记住该表达式以便稍后调用 pull_var_clause。
			 */
			fc_non_group_cols = lappend(fc_non_group_cols, fc_expr);
		}

		fc_i++;
	}

	/*
	 * 如果有 HAVING 条件，我们需要它使用的变量/Aggrefs。
	 */
	if (fc_havingQual)
		fc_non_group_cols = lappend(fc_non_group_cols, fc_havingQual);

	/*
	 * 提取在非分组列（加上 HAVING）中提到的所有变量、PlaceHolderVars 和 Aggrefs，并将它们添加到 partial_target 中（如果尚未存在）。 （直接用作 GROUP BY 项的表达式将已经存在。）注意这包括用于 resjunk 项的变量，因此我们满足了 ORDER BY 和窗口规格的需求。
	 */
	fc_non_group_exprs = pull_var_clause((Node *) fc_non_group_cols,
									  PVC_INCLUDE_AGGREGATES |
									  PVC_RECURSE_WINDOWFUNCS |
									  PVC_INCLUDE_PLACEHOLDERS);

	add_new_columns_to_pathtarget(fc_partial_target, fc_non_group_exprs);

	/*
	 * 调整 Aggrefs 使其处于部分模式。此时所有 Aggrefs 都位于目标列表的顶层，因此我们可以扫描列表，而不是递归遍历表达式树。
	 */
	foreach(fc_lc, fc_partial_target->exprs)
	{
		Aggref	   *fc_aggref = (Aggref *) lfirst(fc_lc);

		if (IsA(fc_aggref, Aggref))
		{
			Aggref	   *fc_newaggref;

			/*
			 * 我们不需要复制 Aggref 节点的子结构，但需要扁平复制节点本身以避免损坏其他树。
			 */
			fc_newaggref = makeNode(Aggref);
			memcpy(fc_newaggref, fc_aggref, sizeof(Aggref));

			/* 目前，假设需要序列化 */
			mark_partial_aggref(fc_newaggref, AGGSPLIT_INITIAL_SERIAL);

			lfirst(fc_lc) = fc_newaggref;
		}
	}

	/* 清理杂项 */
	list_free(fc_non_group_exprs);
	list_free(fc_non_group_cols);

	/* XXX 这导致一些冗余的成本计算…… */
	return set_pathtarget_cost_width(fc_root, fc_partial_target);
}

/*
 * mark_partial_aggref
 *	  调整 Aggref 以使其表示部分聚合步骤。
 *
 * Aggref 节点就地修改；调用者必须进行任何所需的复制。
 */
void mark_partial_aggref(Aggref *fc_agg, AggSplit fc_aggsplit)
{
	/* aggtranstype 应在此时计算 */
	Assert(OidIsValid(fc_agg->aggtranstype));
	/* ... 但 aggsplit 仍应保持为解析器所留内容 */
	Assert(fc_agg->aggsplit == AGGSPLIT_SIMPLE);

	/* 标记 Aggref 以表示预期的部分聚合模式 */
	fc_agg->aggsplit = fc_aggsplit;

	/*
	 * 如有需要，调整结果类型。通常，部分聚合返回
	 * 聚合的过渡类型；但如果它是 INTERNAL 并且我们正在序列化，它会返回 BYTEA。
	 */
	if (DO_AGGSPLIT_SKIPFINAL(fc_aggsplit))
	{
		if (fc_agg->aggtranstype == INTERNALOID && DO_AGGSPLIT_SERIALIZE(fc_aggsplit))
			fc_agg->aggtype = BYTEAOID;
		else
			fc_agg->aggtype = fc_agg->aggtranstype;
	}
}

/*
 * postprocess_setop_tlist
 *	  修复由 plan_set_operations() 返回的目标列表。
 *
 * 我们需要将 orig_tlist 的排序键信息转置到 new_tlist。
 * 注意：如果我们支持对集合操作结果的 resjunk 排序键，则这将不足够
 * —— 那时，我们需要投影整个新 tlist 来评估 resjunk 列。
 * 现在，如果在 orig_tlist 中发现任何 resjunk 列，则只需报错。
 */
static List * fc_postprocess_setop_tlist(List *fc_new_tlist, List *fc_orig_tlist)
{
	ListCell   *fc_l;
	ListCell   *fc_orig_tlist_item = list_head(fc_orig_tlist);

	foreach(fc_l, fc_new_tlist)
	{
		TargetEntry *fc_new_tle = lfirst_node(TargetEntry, fc_l);
		TargetEntry *fc_orig_tle;

		/* 忽略集合操作结果中的 resjunk 列 */
		if (fc_new_tle->resjunk)
			continue;

		Assert(fc_orig_tlist_item != NULL);
		fc_orig_tle = lfirst_node(TargetEntry, fc_orig_tlist_item);
		fc_orig_tlist_item = lnext(fc_orig_tlist, fc_orig_tlist_item);
		if (fc_orig_tle->resjunk)	/* 不应该发生 */
			elog(ERROR, "resjunk output columns are not implemented");
		Assert(fc_new_tle->resno == fc_orig_tle->resno);
		fc_new_tle->ressortgroupref = fc_orig_tle->ressortgroupref;
	}
	if (fc_orig_tlist_item != NULL)
		elog(ERROR, "resjunk output columns are not implemented");
	return fc_new_tlist;
}

/*
 * select_active_windows
 *		创建“活动”窗口子句的列表（即由非删除的 WindowFuncs 引用的子句）
 *		按其执行顺序排序。
 */
static List * fc_select_active_windows(PlannerInfo *fc_root, WindowFuncLists *fc_wflists)
{
	List	   *fc_windowClause = fc_root->parse->windowClause;
	List	   *fc_result = NIL;
	ListCell   *fc_lc;
	int			fc_nActive = 0;
	WindowClauseSortData *fc_actives = palloc(sizeof(WindowClauseSortData)
										   * list_length(fc_windowClause));

	/* 首先，构造活动窗口的数组 */
	foreach(fc_lc, fc_windowClause)
	{
		WindowClause *fc_wc = lfirst_node(WindowClause, fc_lc);

		/* 只有当 wflists 显示一些相关的 WindowFuncs 时，它才是活动的 */
		Assert(fc_wc->winref <= fc_wflists->maxWinRef);
		if (fc_wflists->windowFuncs[fc_wc->winref] == NIL)
			continue;

		fc_actives[fc_nActive].wc = fc_wc;	/* 原始子句 */

		
		fc_actives[fc_nActive].uniqueOrder =
			list_concat_unique(list_copy(fc_wc->partitionClause),
							   fc_wc->orderClause);
		fc_nActive++;
	}

	/*
	 * 按其分区/排序子句对活动窗口进行排序，忽略任何框架子句，以便需要相同排序的窗口在列表中相邻。当我们生成路径时，这将避免插入额外的排序节点。
	 *
	 * 这是我们实现 SQL 标准的特定要求，标准指出当两个或多个窗口是等价顺序时（即具有匹配的分区和排序子句，即使它们的名称或框架子句不同），则所有相邻行必须在所有窗口中以相同的顺序呈现。如果我们允许在这种情况下有多个排序节点，我们将面临相邻行在等价窗口中由于排序不稳定而以不同顺序结束的风险。（参见 SQL2008 - SQL2016 中的 <window clause> 的一般规则 4。）
	 *
	 * 此外，如果一个窗口的整个子句列表是另一个的前缀，则先放置具有更强排序要求的窗口。这样，我们将首先对更强的窗口进行排序，而不必再次对较弱的窗口进行排序。
	 */
	qsort(fc_actives, fc_nActive, sizeof(WindowClauseSortData), fc_common_prefix_cmp);

	/* 构建原始 WindowClause 节点的有序列表 */
	for (int fc_i = 0; fc_i < fc_nActive; fc_i++)
		fc_result = lappend(fc_result, fc_actives[fc_i].wc);

	pfree(fc_actives);

	return fc_result;
}

/*
 * common_prefix_cmp
 *	  QSort 比较函数，用于 WindowClauseSortData
 *
 * 按照所需的排序子句对窗口进行排序。首先，比较排序子句本身。其次，如果一个窗口的子句是另一个窗口的子句的前缀，则将包含更多排序子句的窗口放在前面。
 */
static int fc_common_prefix_cmp(const void *fc_a, const void *fc_b)
{
	const WindowClauseSortData *fc_wcsa = fc_a;
	const WindowClauseSortData *fc_wcsb = fc_b;
	ListCell   *fc_item_a;
	ListCell   *fc_item_b;

	forboth(fc_item_a, fc_wcsa->uniqueOrder, fc_item_b, fc_wcsb->uniqueOrder)
	{
		SortGroupClause *fc_sca = lfirst_node(SortGroupClause, fc_item_a);
		SortGroupClause *fc_scb = lfirst_node(SortGroupClause, fc_item_b);

		if (fc_sca->tleSortGroupRef > fc_scb->tleSortGroupRef)
			return -1;
		else if (fc_sca->tleSortGroupRef < fc_scb->tleSortGroupRef)
			return 1;
		else if (fc_sca->sortop > fc_scb->sortop)
			return -1;
		else if (fc_sca->sortop < fc_scb->sortop)
			return 1;
		else if (fc_sca->nulls_first && !fc_scb->nulls_first)
			return -1;
		else if (!fc_sca->nulls_first && fc_scb->nulls_first)
			return 1;
		/* 不需要比较 eqop，因为它完全由 sortop 决定 */
	}

	if (list_length(fc_wcsa->uniqueOrder) > list_length(fc_wcsb->uniqueOrder))
		return -1;
	else if (list_length(fc_wcsa->uniqueOrder) < list_length(fc_wcsb->uniqueOrder))
		return 1;

	return 0;
}

/*
 * make_window_input_target
 *	  为 WindowAgg 节点生成适当的 PathTarget 初始输入。
 *
 * 当查询包含窗口函数时，此函数计算在第一个 WindowAgg 下方的节点所需计算的目标。此 tlist 必须包含评估窗口函数、计算最终目标列表和执行任何必要的最终排序步骤所需的所有值。如果需要多个 WindowAgg，每个中间的 WindowAgg 都会将其窗口函数结果添加到此基本 tlist 中；只有最上层的 WindowAgg 才计算实际所需的目标列表。
 *
 * 此函数与 make_group_input_target 非常相似，尽管不够像以共享代码。与该函数一样，我们将大多数表达式展开为其组成变量。但我们不想展开窗口的 PARTITION BY/ORDER BY 子句，因为这可能导致对它们的多次评估，这将是坏事（可能甚至导致不一致的答案，如果它们包含易变函数）。此外，我们不得展开 make_group_input_target 没有展开的 GROUP BY 子句，因为我们可能不再能访问其中的各个 Vars。
 *
 * 与 make_group_input_target 的另一个关键区别在于，我们不展开 Aggref 表达式，因为这些是在窗口函数下方计算的，并像上面的 Vars 一样进行引用。
 *
 * 'final_target' 是查询的最终目标列表（以 PathTarget 形式），
 * 'activeWindows' 是之前被 select_active_windows 确定的活动窗口列表。
 *
 * 结果是将由紧接第一个 WindowAgg 节点的计划节点计算的 PathTarget。
 */
static PathTarget * fc_make_window_input_target(PlannerInfo *fc_root,
						 PathTarget *fc_final_target,
						 List *fc_activeWindows)
{
	Query	   *fc_parse = fc_root->parse;
	PathTarget *fc_input_target;
	Bitmapset  *fc_sgrefs;
	List	   *fc_flattenable_cols;
	List	   *fc_flattenable_vars;
	int			fc_i;
	ListCell   *fc_lc;

	Assert(fc_parse->hasWindowFuncs);

	/*
	 * 收集窗口的 PARTITION/ORDER BY 子句的 sortgroupref 数字
	 * 到一个 bitmapset 中，以便于在下面参考。
	 */
	fc_sgrefs = NULL;
	foreach(fc_lc, fc_activeWindows)
	{
		WindowClause *fc_wc = lfirst_node(WindowClause, fc_lc);
		ListCell   *fc_lc2;

		foreach(fc_lc2, fc_wc->partitionClause)
		{
			SortGroupClause *fc_sortcl = lfirst_node(SortGroupClause, fc_lc2);

			fc_sgrefs = bms_add_member(fc_sgrefs, fc_sortcl->tleSortGroupRef);
		}
		foreach(fc_lc2, fc_wc->orderClause)
		{
			SortGroupClause *fc_sortcl = lfirst_node(SortGroupClause, fc_lc2);

			fc_sgrefs = bms_add_member(fc_sgrefs, fc_sortcl->tleSortGroupRef);
		}
	}

	/* 也添加 GROUP BY 子句的 sortgroupref 数字 */
	foreach(fc_lc, fc_parse->groupClause)
	{
		SortGroupClause *fc_grpcl = lfirst_node(SortGroupClause, fc_lc);

		fc_sgrefs = bms_add_member(fc_sgrefs, fc_grpcl->tleSortGroupRef);
	}

	/*
	 * 构造一个包含所有不可展开 targetlist 项的目标，并将其他项目暂时保存。
	 */
	fc_input_target = create_empty_pathtarget();
	fc_flattenable_cols = NIL;

	fc_i = 0;
	foreach(fc_lc, fc_final_target->exprs)
	{
		Expr	   *fc_expr = (Expr *) lfirst(fc_lc);
		Index		fc_sgref = get_pathtarget_sortgroupref(fc_final_target, fc_i);

		/*
		 * 不想解构窗口子句或 GROUP BY 项目。 （注意
		 * 这些项目不能包含窗口函数，因此在 WindowAgg 节点下计算它们是可以的。）
		 */
		if (fc_sgref != 0 && bms_is_member(fc_sgref, fc_sgrefs))
		{
			/*
			 * 不想解构此值，因此将其原样添加到输入
			 * 目标中。
			 */
			add_column_to_pathtarget(fc_input_target, fc_expr, fc_sgref);
		}
		else
		{
			/*
			 * 列将被展平，因此只需记住表达式以备后续调用 pull_var_clause。
			 */
			fc_flattenable_cols = lappend(fc_flattenable_cols, fc_expr);
		}

		fc_i++;
	}

	/*
	 * 从可展平的列中提取所有提到的 Vars 和 Aggrefs，
	 * 如果尚未存在，则将其添加到输入目标中。 （有些可能已经存在，因为它们直接用作窗口/组子句。）
	 *
	 * 注意：在此处使用 PVC_INCLUDE_AGGREGATES 是至关重要的，以便将任何 Aggrefs 放入 Agg 节点的 tlist 中，而不是留到更高层次计算。 另一方面，我们应该递归进入 WindowFuncs，以确保它们的输入表达式可用。
	 */
	fc_flattenable_vars = pull_var_clause((Node *) fc_flattenable_cols,
									   PVC_INCLUDE_AGGREGATES |
									   PVC_RECURSE_WINDOWFUNCS |
									   PVC_INCLUDE_PLACEHOLDERS);
	add_new_columns_to_pathtarget(fc_input_target, fc_flattenable_vars);

	/* 清理杂项 */
	list_free(fc_flattenable_vars);
	list_free(fc_flattenable_cols);

	/* XXX 这导致一些冗余的成本计算…… */
	return set_pathtarget_cost_width(fc_root, fc_input_target);
}

/*
 * make_pathkeys_for_window
 *	 创建一个 pathkeys 列表，描述给定 WindowClause 所需的输入排序。
 *
 * 所需的排序首先是 PARTITION 键，然后是 ORDER 键。
 * 将来我们可能会尝试使用哈希实现窗口，在这种情况下，排序可以放宽，但现在我们总是排序。
 */
static List * fc_make_pathkeys_for_window(PlannerInfo *fc_root, WindowClause *fc_wc,
						 List *fc_tlist)
{
	List	   *fc_window_pathkeys;
	List	   *fc_window_sortclauses;

	/* 如果无法排序则抛出错误 */
	if (!grouping_is_sortable(fc_wc->partitionClause))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("could not implement window PARTITION BY"),
				 errdetail("Window partitioning columns must be of sortable datatypes.")));
	if (!grouping_is_sortable(fc_wc->orderClause))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("could not implement window ORDER BY"),
				 errdetail("Window ordering columns must be of sortable datatypes.")));

	/* 好的，生成组合的 pathkeys */
	fc_window_sortclauses = list_concat_copy(fc_wc->partitionClause, fc_wc->orderClause);
	fc_window_pathkeys = make_pathkeys_for_sortclauses(fc_root,
													fc_window_sortclauses,
													fc_tlist);
	list_free(fc_window_sortclauses);
	return fc_window_pathkeys;
}


/*
 * make_sort_input_target
 *	  生成适用于排序步骤初始输入的 PathTarget。
 *
 * 如果查询有 ORDER BY，这个函数选择由刚好位于 Sort 下方的节点
 * 计算的目标（如果有 DISTINCT，因为 Unique 不能投影）步骤。这可能或可能
 * 不与查询的最终输出目标相同。
 *
 * 保持排序输入 tlist 与最终相同的主要论点是，我们可以避免单独的投影节点
 * （如果它们不同，Sort 不能投影，则需要这个节点）。然而，将 tlist 评估
 * 推迟到 Sort 之后也有其优点：它确保对 tlist 中的任何波动函数有一致的评估
 * 顺序，如果还有 LIMIT，我们可以在从不计算后续行的 tlist 函数的情况下停止
 * 查询，这对波动和昂贵的函数都有利。
 *
 * 我们当前的政策是，无条件地将波动表达式推迟到排序之后（假设这可能，即
 * 它们位于普通 tlist 列中而不是 ORDER BY/GROUP BY/DISTINCT 列中）。我们
 * 也倾向于推迟集合返回表达式，因为提前运行它们会膨胀排序数据集，并且如果
 * 排序不稳定，它可能会导致意外的输出顺序。然而对此有一个限制：tlist 中
 * 所有 SRF 必须在同一步骤中评估，以便它们可以在 nodeProjectSet 中同步运行。
 * 因此，如果任何 SRF 在排序列中，我们就不能推迟任何 SRF。（注意，原则上
 * 这个政策可能也应该应用于 group/window 输入目标列表，但我们历史上并未
 * 这样做。）最后，如果存在 LIMIT，或者如果 root->tuple_fraction 显示部分
 * 评估查询是可能的（如果两者都不成立，我们仍然预计每行都要评估这些表达式），
 * 或者如果存在任何波动或集合返回表达式（因为一旦我们进行投影，就不会
 * 额外花费推迟更多内容的成本）时，就会推迟昂贵的表达式。
 *
 * 这里可能还考虑的另一个问题是，评估 tlist 表达式可能导致数据的宽度
 * 或狭度超过输入 Vars，从而改变必须通过排序的数据量。然而，我们通常
 * 对于任何比 Var 更复杂的表达式的输出宽度只有非常糟糕的想法，因此
 * 目前似乎很冒险尝试在这个基础上进行优化。
 *
 * 请注意，如果我们确实生成了修改后的排序输入目标，而查询最终没有
 * 使用显式排序，将不会造成任何特别的伤害：我们会最初对前面的路径节点使用
 * 修改后的目标，但随后将其更改为通过 apply_projection_to_path 得到的
 * 最终目标。此外，在这种情况下，关于波动函数评估顺序的保证仍然成立，
 * 因为行已经被排序。
 *
 * 这个函数与 make_group_input_target 和 make_window_input_target 有一些共同点，
 * 尽管详细规则有所不同。我们决不会扁平化/推迟任何分组或排序列；这些在
 * 排序之前是需要的。如果我们确实扁平化了特定的表达式，我们会保持 Aggref
 * 和 WindowFunc 节点不变，因为这些是在早期计算的。
 *
 * 'final_target' 是查询的最终目标列表（以 PathTarget 形式）
 * 'have_postponed_srfs' 是一个输出参数，见下文
 *
 * 结果是要由紧挨着 Sort 步骤的计划节点计算的 PathTarget
 * （如果有 Distinct 步骤）。如果我们决定投影步骤没有帮助，
 * 那么这将正好是 final_target。
 *
 * 此外，如果我们选择将任何集合返回函数推迟到 Sort 之后，
 * *have_postponed_srfs 被设置为 true。
 */
static PathTarget * fc_make_sort_input_target(PlannerInfo *fc_root,
					   PathTarget *fc_final_target,
					   bool *fc_have_postponed_srfs)
{
	Query	   *fc_parse = fc_root->parse;
	PathTarget *fc_input_target;
	int			fc_ncols;
	bool	   *fc_col_is_srf;
	bool	   *fc_postpone_col;
	bool		fc_have_srf;
	bool		fc_have_volatile;
	bool		fc_have_expensive;
	bool		fc_have_srf_sortcols;
	bool		fc_postpone_srfs;
	List	   *fc_postponable_cols;
	List	   *fc_postponable_vars;
	int			fc_i;
	ListCell   *fc_lc;

	/* 除非查询有 ORDER BY，否则不应到此 */
	Assert(fc_parse->sortClause);

	*fc_have_postponed_srfs = false;	/* 默认结果 */

	/* 检查 tlist 并收集按列的信息 */
	fc_ncols = list_length(fc_final_target->exprs);
	fc_col_is_srf = (bool *) palloc0(fc_ncols * sizeof(bool));
	fc_postpone_col = (bool *) palloc0(fc_ncols * sizeof(bool));
	fc_have_srf = fc_have_volatile = fc_have_expensive = fc_have_srf_sortcols = false;

	fc_i = 0;
	foreach(fc_lc, fc_final_target->exprs)
	{
		Expr	   *fc_expr = (Expr *) lfirst(fc_lc);

		/*
		 * 如果列有 sortgroupref，则假设必须在排序之前进行评估。
		 * 通常这些列是 ORDER BY、GROUP BY 等目标。一个例外是
		 * 被 remove_useless_groupby_columns() 移除的 GROUP BY 列……
		 * 但那些列无论如何都是 Vars。似乎没有什么案例值得
		 * 再次确认。
		 */
		if (get_pathtarget_sortgroupref(fc_final_target, fc_i) == 0)
		{
			/*
			 * 检查 SRF 或波动函数。首先检查 SRF 情况，因为
			 * 我们必须知道是否有任何被推迟的 SRF。
			 */
			if (fc_parse->hasTargetSRFs &&
				expression_returns_set((Node *) fc_expr))
			{
				/* 我们将在下面决定这些是否可以推迟 */
				fc_col_is_srf[fc_i] = true;
				fc_have_srf = true;
			}
			else if (contain_volatile_functions((Node *) fc_expr))
			{
				/* 无条件推迟 */
				fc_postpone_col[fc_i] = true;
				fc_have_volatile = true;
			}
			else
			{
				/*
				 * 否则检查成本。XXX 当 set_pathtarget_cost_width() 刚刚
				 * 做到这一点时，必须这样做令人恼火。重构以允许共享工作？
				 */
				QualCost	fc_cost;

				cost_qual_eval_node(&fc_cost, (Node *) fc_expr, fc_root);

				
/*
				 * 我们任意定义“昂贵”为“超过10倍的
				 * cpu_operator_cost”。注意这将接受任何具有默认成本的PL函数。
				 */
				if (fc_cost.per_tuple > 10 * cpu_operator_cost)
				{
					fc_postpone_col[fc_i] = true;
					fc_have_expensive = true;
				}
			}
		}
		else
		{
			/* 对于sortgroupref列，仅检查是否有包含SRF的列 */
			if (!fc_have_srf_sortcols &&
				fc_parse->hasTargetSRFs &&
				expression_returns_set((Node *) fc_expr))
				fc_have_srf_sortcols = true;
		}

		fc_i++;
	}

	/*
	 * 如果我们有一些SRF但没有在sortgroupref列中，可以推迟SRF。
	 */
	fc_postpone_srfs = (fc_have_srf && !fc_have_srf_sortcols);

	/*
	 * 如果我们不需要后排序投影，只需返回final_target。
	 */
	if (!(fc_postpone_srfs || fc_have_volatile ||
		  (fc_have_expensive &&
		   (fc_parse->limitCount || fc_root->tuple_fraction > 0))))
		return fc_final_target;

	/*
	 * 报告后排序投影是否将包含集返回函数。这很重要，因为它影响Sort是否可以依赖查询的LIMIT（如果有的话）来限制所需返回的行数。
	 */
	*fc_have_postponed_srfs = fc_postpone_srfs;

	/*
	 * 构建排序输入目标，首先获取所有不可推迟的列，然后添加在可推迟的列中发现的Vars、PlaceHolderVars、Aggrefs和WindowFuncs。
	 */
	fc_input_target = create_empty_pathtarget();
	fc_postponable_cols = NIL;

	fc_i = 0;
	foreach(fc_lc, fc_final_target->exprs)
	{
		Expr	   *fc_expr = (Expr *) lfirst(fc_lc);

		if (fc_postpone_col[fc_i] || (fc_postpone_srfs && fc_col_is_srf[fc_i]))
			fc_postponable_cols = lappend(fc_postponable_cols, fc_expr);
		else
			add_column_to_pathtarget(fc_input_target, fc_expr,
									 get_pathtarget_sortgroupref(fc_final_target, fc_i));

		fc_i++;
	}

	/*
	 * 提取在可推迟列中提到的所有Vars、Aggrefs和WindowFuncs，并如果尚未存在则将它们添加到排序输入目标中。（其中一些可能已经存在。）我们不能在这里解构Aggrefs或WindowFuncs，因为投影节点将无法重新计算它们。
	 */
	fc_postponable_vars = pull_var_clause((Node *) fc_postponable_cols,
									   PVC_INCLUDE_AGGREGATES |
									   PVC_INCLUDE_WINDOWFUNCS |
									   PVC_INCLUDE_PLACEHOLDERS);
	add_new_columns_to_pathtarget(fc_input_target, fc_postponable_vars);

	/* 清理杂项 */
	list_free(fc_postponable_vars);
	list_free(fc_postponable_cols);

	/* XXX 这表示更冗余的成本计算... */
	return set_pathtarget_cost_width(fc_root, fc_input_target);
}

/*
 * get_cheapest_fractional_path
 *	  查找获取指定比例的所有
 *	  预计将由给定关系返回的元组的最便宜路径。
 *
 * 我们以与grouping_planner相同的方式解释tuple_fraction。
 *
 * 我们假设set_cheapest()已在给定关系上运行。
 */
Path * get_cheapest_fractional_path(RelOptInfo *fc_rel, double fc_tuple_fraction)
{
	Path	   *fc_best_path = fc_rel->cheapest_total_path;
	ListCell   *fc_l;

	/* 如果要检索所有元组，直接返回最便宜的总路径 */
	if (fc_tuple_fraction <= 0.0)
		return fc_best_path;

	/* 将绝对的元组数量转换为一个分数；不需要限制在0..1之间 */
	if (fc_tuple_fraction >= 1.0 && fc_best_path->rows > 0)
		fc_tuple_fraction /= fc_best_path->rows;

	foreach(fc_l, fc_rel->pathlist)
	{
		Path	   *fc_path = (Path *) lfirst(fc_l);

		if (fc_path == fc_rel->cheapest_total_path ||
			compare_fractional_path_costs(fc_best_path, fc_path, fc_tuple_fraction) <= 0)
			continue;

		fc_best_path = fc_path;
	}

	return fc_best_path;
}

/*
 * adjust_paths_for_srfs
 *		修正给定上层关系的路径，以正确处理tSRFs。
 *
 * 执行器只能处理出现在ProjectSet计划节点目标列表顶层的集合返回函数（SRFs）。如果我们有任何不在顶层的SRFs，我们需要将评估拆分为多个计划级别，每个级别满足此约束。此函数修改可能在其输出目标列表中计算任何SRFs的上层关系的每个路径，以插入适当的投影步骤。
 *
 * 给定的targets和targets_contain_srfs列表来自split_pathtarget_at_srfs()。我们假设现有的路径发出目标中的第一个目标。
 */
static void fc_adjust_paths_for_srfs(PlannerInfo *fc_root, RelOptInfo *fc_rel,
					  List *fc_targets, List *fc_targets_contain_srfs)
{
	ListCell   *fc_lc;

	Assert(list_length(fc_targets) == list_length(fc_targets_contain_srfs));
	Assert(!linitial_int(fc_targets_contain_srfs));

	/* 如果此计划级别没有SRFs，则无需操作 */
	if (list_length(fc_targets) == 1)
		return;

	/*
	 * 在每个路径上叠加SRF评估节点。
	 *
	 * 从原则上讲，我们应该在这里重新运行set_cheapest()以识别最便宜的路径，但添加相同的目标列表评估成本到所有路径似乎不太可能会改变这一点，因此我们不必这样做。相反，仅假设最便宜的启动和最便宜的总路径保持不变。（应该没有参数化路径，因此我们不必担心更新最便宜的参数化路径。）
	 */
	foreach(fc_lc, fc_rel->pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);
		Path	   *fc_newpath = fc_subpath;
		ListCell   *fc_lc1,
				   *fc_lc2;

		Assert(fc_subpath->param_info == NULL);
		forboth(fc_lc1, fc_targets, fc_lc2, fc_targets_contain_srfs)
		{
			PathTarget *fc_thistarget = lfirst_node(PathTarget, fc_lc1);
			bool		fc_contains_srfs = (bool) lfirst_int(fc_lc2);

			/* 如果这一层不包含SRFs，则进行常规投影 */
			if (fc_contains_srfs)
				fc_newpath = (Path *) create_set_projection_path(fc_root,
															  fc_rel,
															  fc_newpath,
															  fc_thistarget);
			else
				fc_newpath = (Path *) apply_projection_to_path(fc_root,
															fc_rel,
															fc_newpath,
															fc_thistarget);
		}
		lfirst(fc_lc) = fc_newpath;
		if (fc_subpath == fc_rel->cheapest_startup_path)
			fc_rel->cheapest_startup_path = fc_newpath;
		if (fc_subpath == fc_rel->cheapest_total_path)
			fc_rel->cheapest_total_path = fc_newpath;
	}

	/* 对于部分路径（如果有），同样适用 */
	foreach(fc_lc, fc_rel->partial_pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);
		Path	   *fc_newpath = fc_subpath;
		ListCell   *fc_lc1,
				   *fc_lc2;

		Assert(fc_subpath->param_info == NULL);
		forboth(fc_lc1, fc_targets, fc_lc2, fc_targets_contain_srfs)
		{
			PathTarget *fc_thistarget = lfirst_node(PathTarget, fc_lc1);
			bool		fc_contains_srfs = (bool) lfirst_int(fc_lc2);

			/* 如果这一层不包含SRFs，则进行常规投影 */
			if (fc_contains_srfs)
				fc_newpath = (Path *) create_set_projection_path(fc_root,
															  fc_rel,
															  fc_newpath,
															  fc_thistarget);
			else
			{
				/* 避免对路径应用投影，以防多重引用 */
				fc_newpath = (Path *) create_projection_path(fc_root,
														  fc_rel,
														  fc_newpath,
														  fc_thistarget);
			}
		}
		lfirst(fc_lc) = fc_newpath;
	}
}

/*
 * expression_planner
 *		对独立表达式执行规划器的转换。
 *
 * 各种实用命令需要评估不属于可规划查询的表达式。它们可以使用执行器的常规表达式执行机制，但首先必须通过此处处理，将其从解析器输出转换为可执行的内容。
 *
 * 目前，我们不允许独立表达式中出现子链接，因此这里没有真正的“规划”。（不过，这种情况可能并不总是正确的。）我们必须运行eval_const_expressions以确保任何函数调用被转换为位置符号，并插入函数默认参数。常量子表达式被简化的事实是一个副作用，当表达式将被评估多次时，这种副作用是有用的。此外，我们必须修复操作符函数ID。
 *
 * 这不会返回任何关于表达式依赖关系的信息。因此调用者应仅在当前查询的持续时间内使用结果。希望缓存结果更长时间的调用者应使用expression_planner_with_deps，可能通过plancache。
 *
 * 注意：这不得对传入的表达式树进行任何有害的更改。（实际上可以对其应用fix_opfuncids，但由于我们首先进行基于expression_tree_mutator的遍历，返回的将是一个新的节点树。）结果在当前内存上下文中构造；要注意这可能在其中泄漏大量附加内容。
 */
Expr * expression_planner(Expr *fc_expr)
{
	Node	   *fc_result;

	/*
	 * 转换命名参数函数调用，插入默认参数并简化常量子表达式
	 */
	fc_result = eval_const_expressions(NULL, (Node *) fc_expr);

	/* 如果缺失，则填充opfuncid值 */
	fix_opfuncids(fc_result);

	return (Expr *) fc_result;
}

/*
 * expression_planner_with_deps
 *		对独立表达式执行规划器的转换，同时返回表达式依赖信息和结果。
 *
 * 这与expression_planner()相同，只是它还返回有关表达式可能依赖关系的信息，即定义影响结果的对象的标识。与PlannedStmt一样，这些依赖关系以关系OID列表和PlanInvalItems列表的形式表示。
 */
Expr * expression_planner_with_deps(Expr *fc_expr,
							 List **fc_relationOids,
							 List **fc_invalItems)
{
	Node	   *fc_result;
	PlannerGlobal fc_glob;
	PlannerInfo fc_root;

	/* 制作虚拟的规划器状态，以便我们可以使用setrefs机制 */
	MemSet(&fc_glob, 0, sizeof(fc_glob));
	fc_glob.type = T_PlannerGlobal;
	fc_glob.relationOids = NIL;
	fc_glob.invalItems = NIL;

	MemSet(&fc_root, 0, sizeof(fc_root));
	fc_root.type = T_PlannerInfo;
	fc_root.glob = &fc_glob;

	/*
	 * 转换命名参数函数调用，插入默认参数并简化常量子表达式。还收集内联函数和省略域的标识。
	 */
	fc_result = eval_const_expressions(&fc_root, (Node *) fc_expr);

	/* 如果缺失，则填充opfuncid值 */
	fix_opfuncids(fc_result);

	/*
	 * 现在遍历完成的表达式以查找我们应该记录的其他任何表达式依赖关系。
	 */
	(void) extract_query_dependencies_walker(fc_result, &fc_root);

	*fc_relationOids = fc_glob.relationOids;
	*fc_invalItems = fc_glob.invalItems;

	return (Expr *) fc_result;
}


/*
 * plan_cluster_use_sort
 *		使用规划器决定CLUSTER应该如何实现排序
 *
 * tableOid是要在其索引indexOid上进行聚类的表的OID（已知为btree索引）。决定进行索引扫描还是顺序扫描加排序来执行CLUSTER哪个更便宜。返回true以使用排序，返回false以使用索引扫描。
 *
 * 注意：调用者最好已经对表持有某种类型的锁。
 */
bool plan_cluster_use_sort(Oid fc_tableOid, Oid fc_indexOid)
{
	PlannerInfo *fc_root;
	Query	   *fc_query;
	PlannerGlobal *fc_glob;
	RangeTblEntry *fc_rte;
	RelOptInfo *fc_rel;
	IndexOptInfo *fc_indexInfo;
	QualCost	fc_indexExprCost;
	Cost		fc_comparisonCost;
	Path	   *fc_seqScanPath;
	Path		fc_seqScanAndSortPath;
	IndexPath  *fc_indexScanPath;
	ListCell   *fc_lc;

	/* 如果禁用了索引扫描，我们可以短路成本比较 */
	if (!enable_indexscan)
		return true;			/* use sort */

	/* 设置大多数为虚拟的规划器状态 */
	fc_query = makeNode(Query);
	fc_query->commandType = CMD_SELECT;

	fc_glob = makeNode(PlannerGlobal);

	fc_root = makeNode(PlannerInfo);
	fc_root->parse = fc_query;
	fc_root->glob = fc_glob;
	fc_root->query_level = 1;
	fc_root->planner_cxt = CurrentMemoryContext;
	fc_root->wt_param_id = -1;

	/* 为关系构建一个最小的RTE */
	fc_rte = makeNode(RangeTblEntry);
	fc_rte->rtekind = RTE_RELATION;
	fc_rte->relid = fc_tableOid;
	fc_rte->relkind = RELKIND_RELATION;	/* 不要过于挑剔。 */
	fc_rte->rellockmode = AccessShareLock;
	fc_rte->lateral = false;
	fc_rte->inh = false;
	fc_rte->inFromCl = true;
	fc_query->rtable = list_make1(fc_rte);

	
/* 设置 RTE/RelOptInfo 数组 */
	setup_simple_rel_arrays(fc_root);

	/* 构建 RelOptInfo */
	fc_rel = build_simple_rel(fc_root, 1, NULL);

	/* 定位目标索引的 IndexOptInfo */
	fc_indexInfo = NULL;
	foreach(fc_lc, fc_rel->indexlist)
	{
		fc_indexInfo = lfirst_node(IndexOptInfo, fc_lc);
		if (fc_indexInfo->indexoid == fc_indexOid)
			break;
	}

	/*
	 * 有可能 get_relation_info 没有为所需索引生成 IndexOptInfo；
	 * 这可能发生在它尚未达到 indcheckxmin 可用性视野，或如果它是系统索引而我们
	 * 忽略了系统索引。在这种情况下，我们应该告诉 CLUSTER 不
	 * 信任索引内容，而是使用 seqscan-and-sort。
	 */
	if (fc_lc == NULL)				/* 不在列表中？ */
		return true;			/* use sort */

	/*
	 * 与其做所有需要的推演以使用 set_baserel_size_estimates，不如
	 * 对行和宽度做一个快速黑客处理。
	 */
	fc_rel->rows = fc_rel->tuples;
	fc_rel->reltarget->width = get_relation_data_width(fc_tableOid, NULL);

	fc_root->total_table_pages = fc_rel->pages;

	/*
	 * 确定索引表达式的评估成本（如果有的话）。我们需要
	 * 对每个在排序过程中发生的元组比较收取两倍的费用，
	 * 因为 tuplesort.c 每次都需要重新评估索引
	 * 表达式。（XXX 这真是效率不高……）
	 */
	cost_qual_eval(&fc_indexExprCost, fc_indexInfo->indexprs, fc_root);
	fc_comparisonCost = 2.0 * (fc_indexExprCost.startup + fc_indexExprCost.per_tuple);

	/* 估算 seq scan + sort 的成本 */
	fc_seqScanPath = create_seqscan_path(fc_root, fc_rel, NULL, 0);
	cost_sort(&fc_seqScanAndSortPath, fc_root, NIL,
			  fc_seqScanPath->total_cost, fc_rel->tuples, fc_rel->reltarget->width,
			  fc_comparisonCost, maintenance_work_mem, -1.0);

	/* 估算索引扫描的成本 */
	fc_indexScanPath = create_index_path(fc_root, fc_indexInfo,
									  NIL, NIL, NIL, NIL,
									  ForwardScanDirection, false,
									  NULL, 1.0, false);

	return (fc_seqScanAndSortPath.total_cost < fc_indexScanPath->path.total_cost);
}

/*
 * plan_create_index_workers
 *		使用规划者来决定 CREATE INDEX 应请求多少个并行工作进程
 *
 * tableOid 是要构建索引的表，indexOid 是要创建或重新索引的索引的 OID（必须是 btree 索引）。
 *
 * 返回值是请求的并行工作进程数量。如果这个值为 0，则继续可能不安全。请注意，这不包括作为工作者参与的领导者（值始终是并行工作进程的数量）。
 *
 * 注意：调用者最好已经对表和索引持有某种类型的锁。
 */
int plan_create_index_workers(Oid fc_tableOid, Oid fc_indexOid)
{
	PlannerInfo *fc_root;
	Query	   *fc_query;
	PlannerGlobal *fc_glob;
	RangeTblEntry *fc_rte;
	Relation	fc_heap;
	Relation	fc_index;
	RelOptInfo *fc_rel;
	int			fc_parallel_workers;
	BlockNumber fc_heap_blocks;
	double		fc_reltuples;
	double		fc_allvisfrac;

	/*
	 * 我们不允许在独立后端或禁用并行时执行并行操作。
	 */
	if (!IsUnderPostmaster || max_parallel_maintenance_workers == 0)
		return 0;

	/* 设置基本的虚拟规划状态 */
	fc_query = makeNode(Query);
	fc_query->commandType = CMD_SELECT;

	fc_glob = makeNode(PlannerGlobal);

	fc_root = makeNode(PlannerInfo);
	fc_root->parse = fc_query;
	fc_root->glob = fc_glob;
	fc_root->query_level = 1;
	fc_root->planner_cxt = CurrentMemoryContext;
	fc_root->wt_param_id = -1;

	/*
	 * 构建一个最小的 RTE。
	 *
	 * 将 RTE 标记为 inh = true。这是一个变通方法，防止
	 * get_relation_info() 获取索引信息，这是必要的，
	 * 因为它不期望任何 IndexOptInfo 当前正在
	 * 进行 REINDEX。
	 */
	fc_rte = makeNode(RangeTblEntry);
	fc_rte->rtekind = RTE_RELATION;
	fc_rte->relid = fc_tableOid;
	fc_rte->relkind = RELKIND_RELATION;	/* 不要过于挑剔。 */
	fc_rte->rellockmode = AccessShareLock;
	fc_rte->lateral = false;
	fc_rte->inh = true;
	fc_rte->inFromCl = true;
	fc_query->rtable = list_make1(fc_rte);

	
/* 设置 RTE/RelOptInfo 数组 */
	setup_simple_rel_arrays(fc_root);

	/* 构建 RelOptInfo */
	fc_rel = build_simple_rel(fc_root, 1, NULL);

	/* 假定 Rels 已经被调用者锁定 */
	fc_heap = table_open(fc_tableOid, NoLock);
	fc_index = index_open(fc_indexOid, NoLock);

	/*
	 * 确定是否可以安全进行。
	 *
	 * 目前，平行工作者无法访问领导者的临时表。
	 * 此外，任何索引谓词或索引表达式必须是并行
	 * 安全的。
	 */
	if (fc_heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
		!is_parallel_safe(fc_root, (Node *) RelationGetIndexExpressions(fc_index)) ||
		!is_parallel_safe(fc_root, (Node *) RelationGetIndexPredicate(fc_index)))
	{
		fc_parallel_workers = 0;
		goto done;
	}

	/*
	 * 如果为表设置了 parallel_workers 存储参数，则接受该
	 * 值作为启动的平行工作进程的数量（尽管仍然限制
	 * 在 max_parallel_maintenance_workers）。注意我们故意不
	 * 考虑 parallel_workers 设置时的其他任何因素。（例如，工作人员的内存使用。）
	 */
	if (fc_rel->rel_parallel_workers != -1)
	{
		fc_parallel_workers = Min(fc_rel->rel_parallel_workers,
							   max_parallel_maintenance_workers);
		goto done;
	}

	/*
	 * 自行估算堆关系大小，因为 rel->pages 不能被
	 * 信任（堆 RTE 被标记为继承父级）
	 */
	estimate_rel_size(fc_heap, NULL, &fc_heap_blocks, &fc_reltuples, &fc_allvisfrac);

	/*
	 * 使用通用模型确定扫描堆关系的工作者数量
	 */
	fc_parallel_workers = compute_parallel_worker(fc_rel, fc_heap_blocks, -1,
											   max_parallel_maintenance_workers);

	/*
	 * 根据可用的 maintenance_work_mem 进行限制。
	 *
	 * 注意，每个 tuplesort 参与者将获得总
	 * maintenance_work_mem 预算的均等份额。目标是让参与者
	 * （包括作为参与者的领导者）拥有不少于 32MB 的
	 * 内存。这在 maintenance_work_mem 设置为 64MB
	 * 时，立即处于能够启动单个平行工作者进行排序的阀值。
	 */
	while (fc_parallel_workers > 0 &&
		   maintenance_work_mem / (fc_parallel_workers + 1) < 32768L)
		fc_parallel_workers--;

done:
	index_close(fc_index, NoLock);
	table_close(fc_heap, NoLock);

	return fc_parallel_workers;
}

/*
 * add_paths_to_grouping_rel
 *
 * 将非部分路径添加到分组关系。
 */
static void add_paths_to_grouping_rel(PlannerInfo *fc_root, RelOptInfo *fc_input_rel,
						  RelOptInfo *fc_grouped_rel,
						  RelOptInfo *fc_partially_grouped_rel,
						  const AggClauseCosts *fc_agg_costs,
						  grouping_sets_data *fc_gd, double fc_dNumGroups,
						  GroupPathExtraData *fc_extra)
{
	Query	   *fc_parse = fc_root->parse;
	Path	   *fc_cheapest_path = fc_input_rel->cheapest_total_path;
	ListCell   *fc_lc;
	bool		fc_can_hash = (fc_extra->flags & GROUPING_CAN_USE_HASH) != 0;
	bool		fc_can_sort = (fc_extra->flags & GROUPING_CAN_USE_SORT) != 0;
	List	   *fc_havingQual = (List *) fc_extra->havingQual;
	AggClauseCosts *fc_agg_final_costs = &fc_extra->agg_final_costs;

	if (fc_can_sort)
	{
		/*
		 * 使用任何可用的适当排序路径作为输入，以及还考虑
		 * 对最低总成本路径进行排序。
		 */
		foreach(fc_lc, fc_input_rel->pathlist)
		{
			Path	   *fc_path = (Path *) lfirst(fc_lc);
			Path	   *fc_path_original = fc_path;
			bool		fc_is_sorted;
			int			fc_presorted_keys;

			fc_is_sorted = pathkeys_count_contained_in(fc_root->group_pathkeys,
													fc_path->pathkeys,
													&fc_presorted_keys);

			if (fc_path == fc_cheapest_path || fc_is_sorted)
			{
				/* 如果最低总成本路径尚未排序，则对其进行排序 */
				if (!fc_is_sorted)
					fc_path = (Path *) create_sort_path(fc_root,
													 fc_grouped_rel,
													 fc_path,
													 fc_root->group_pathkeys,
													 -1.0);

				/* 现在决定在其顶部添加什么 */
				if (fc_parse->groupingSets)
				{
					fc_consider_groupingsets_paths(fc_root, fc_grouped_rel,
												fc_path, true, fc_can_hash,
												fc_gd, fc_agg_costs, fc_dNumGroups);
				}
				else if (fc_parse->hasAggs)
				{
					/*
					 * 我们有聚合，可能带有简单的 GROUP BY。创建
					 * AggPath。
					 */
					add_path(fc_grouped_rel, (Path *)
							 create_agg_path(fc_root,
											 fc_grouped_rel,
											 fc_path,
											 fc_grouped_rel->reltarget,
											 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
											 AGGSPLIT_SIMPLE,
											 fc_parse->groupClause,
											 fc_havingQual,
											 fc_agg_costs,
											 fc_dNumGroups));
				}
				else if (fc_parse->groupClause)
				{
					/*
					 * 我们有 GROUP BY 但没有聚合或分组集。
					 * 创建一个 GroupPath。
					 */
					add_path(fc_grouped_rel, (Path *)
							 create_group_path(fc_root,
											   fc_grouped_rel,
											   fc_path,
											   fc_parse->groupClause,
											   fc_havingQual,
											   fc_dNumGroups));
				}
				else
				{
					/* 其他情况应该在上述处理中已经处理 */
					Assert(false);
				}
			}

			/*
			 * 现在我们可以考虑在这个路径上进行增量排序，但仅在
			 * 路径尚未排序且启用了增量排序时。
			 */
			if (fc_is_sorted || !enable_incremental_sort)
				continue;

			/* 恢复输入路径（我们可能在顶部添加了 Sort）。 */
			fc_path = fc_path_original;

			/* 无共享前缀，没有必要构建增量排序 */
			if (fc_presorted_keys == 0)
				continue;

			/*
			 * 我们应该已经排除了长度为 1 的 pathkeys，因为
			 * 然后 presorted_keys > 0 将意味着 is_sorted 为 true。
			 */
			Assert(list_length(fc_root->group_pathkeys) != 1);

			fc_path = (Path *) create_incremental_sort_path(fc_root,
														 fc_grouped_rel,
														 fc_path,
														 fc_root->group_pathkeys,
														 fc_presorted_keys,
														 -1.0);

			/* 现在决定在其顶部添加什么 */
			if (fc_parse->groupingSets)
			{
				fc_consider_groupingsets_paths(fc_root, fc_grouped_rel,
											fc_path, true, fc_can_hash,
											fc_gd, fc_agg_costs, fc_dNumGroups);
			}
			else if (fc_parse->hasAggs)
			{
				/*
				 * 我们有聚合，可能带有简单的 GROUP BY。创建一个
				 * AggPath。
				 */
				add_path(fc_grouped_rel, (Path *)
						 create_agg_path(fc_root,
										 fc_grouped_rel,
										 fc_path,
										 fc_grouped_rel->reltarget,
										 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
										 AGGSPLIT_SIMPLE,
										 fc_parse->groupClause,
										 fc_havingQual,
										 fc_agg_costs,
										 fc_dNumGroups));
			}
			else if (fc_parse->groupClause)
			{
				/*
				 * 我们有 GROUP BY 但没有聚合或分组集。创建
				 * 一个 GroupPath。
				 */
				add_path(fc_grouped_rel, (Path *)
						 create_group_path(fc_root,
										   fc_grouped_rel,
										   fc_path,
										   fc_parse->groupClause,
										   fc_havingQual,
										   fc_dNumGroups));
			}
			else
			{
				/* 其他情况应该在上述处理中已经处理 */
				Assert(false);
			}
		}

		/*
		 * 我们可以考虑最终化一个部分聚合路径，而不是直接对
		 * 输入关系进行操作。
		 */
		if (fc_partially_grouped_rel != NULL)
		{
			foreach(fc_lc, fc_partially_grouped_rel->pathlist)
			{
				Path	   *fc_path = (Path *) lfirst(fc_lc);
				Path	   *fc_path_original = fc_path;
				bool		fc_is_sorted;
				int			fc_presorted_keys;

				fc_is_sorted = pathkeys_count_contained_in(fc_root->group_pathkeys,
														fc_path->pathkeys,
														&fc_presorted_keys);

				/*
				 * 如果需要，插入一个 Sort 节点。但除了最低成本路径
				 * 外，没有意义排序任何内容。
				 */
				if (!fc_is_sorted)
				{
					if (fc_path != fc_partially_grouped_rel->cheapest_total_path)
						continue;
					fc_path = (Path *) create_sort_path(fc_root,
													 fc_grouped_rel,
													 fc_path,
													 fc_root->group_pathkeys,
													 -1.0);
				}

				if (fc_parse->hasAggs)
					add_path(fc_grouped_rel, (Path *)
							 create_agg_path(fc_root,
											 fc_grouped_rel,
											 fc_path,
											 fc_grouped_rel->reltarget,
											 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
											 AGGSPLIT_FINAL_DESERIAL,
											 fc_parse->groupClause,
											 fc_havingQual,
											 fc_agg_final_costs,
											 fc_dNumGroups));
				else
					add_path(fc_grouped_rel, (Path *)
							 create_group_path(fc_root,
											   fc_grouped_rel,
											   fc_path,
											   fc_parse->groupClause,
											   fc_havingQual,
											   fc_dNumGroups));

				/*
				 * 现在我们可以考虑在这个路径上进行增量排序，但仅在
				 * 路径尚未排序且启用了增量排序时。
				 */
				if (fc_is_sorted || !enable_incremental_sort)
					continue;

				/* 恢复输入路径（我们可能在顶部添加了 Sort）。 */
				fc_path = fc_path_original;

				/* 无共享前缀，构建增量排序没有意义 */
				if (fc_presorted_keys == 0)
					continue;

				/*
				 * 我们应该已经排除了长度为1的路径键，因为那样presorted_keys > 0将意味着is_sorted为true。
				 */
				Assert(list_length(fc_root->group_pathkeys) != 1);

				fc_path = (Path *) create_incremental_sort_path(fc_root,
															 fc_grouped_rel,
															 fc_path,
															 fc_root->group_pathkeys,
															 fc_presorted_keys,
															 -1.0);

				if (fc_parse->hasAggs)
					add_path(fc_grouped_rel, (Path *)
							 create_agg_path(fc_root,
											 fc_grouped_rel,
											 fc_path,
											 fc_grouped_rel->reltarget,
											 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
											 AGGSPLIT_FINAL_DESERIAL,
											 fc_parse->groupClause,
											 fc_havingQual,
											 fc_agg_final_costs,
											 fc_dNumGroups));
				else
					add_path(fc_grouped_rel, (Path *)
							 create_group_path(fc_root,
											   fc_grouped_rel,
											   fc_path,
											   fc_parse->groupClause,
											   fc_havingQual,
											   fc_dNumGroups));
			}
		}
	}

	if (fc_can_hash)
	{
		if (fc_parse->groupingSets)
		{
			/*
			 * 尝试在未排序的输入上进行仅哈希的分组集路径。
			 */
			fc_consider_groupingsets_paths(fc_root, fc_grouped_rel,
										fc_cheapest_path, false, true,
										fc_gd, fc_agg_costs, fc_dNumGroups);
		}
		else
		{
			/*
			 * 生成一个 HashAgg 路径。我们只需要对
			 * 最低总成本输入路径进行聚合，因为输入顺序无关紧要。
			 */
			add_path(fc_grouped_rel, (Path *)
					 create_agg_path(fc_root, fc_grouped_rel,
									 fc_cheapest_path,
									 fc_grouped_rel->reltarget,
									 AGG_HASHED,
									 AGGSPLIT_SIMPLE,
									 fc_parse->groupClause,
									 fc_havingQual,
									 fc_agg_costs,
									 fc_dNumGroups));
		}

		/*
		 * 在最低部分聚合路径的顶部生成一个最终 HashAgg 路径，
		 * 假设存在一个
		 */
		if (fc_partially_grouped_rel && fc_partially_grouped_rel->pathlist)
		{
			Path	   *fc_path = fc_partially_grouped_rel->cheapest_total_path;

			add_path(fc_grouped_rel, (Path *)
					 create_agg_path(fc_root,
									 fc_grouped_rel,
									 fc_path,
									 fc_grouped_rel->reltarget,
									 AGG_HASHED,
									 AGGSPLIT_FINAL_DESERIAL,
									 fc_parse->groupClause,
									 fc_havingQual,
									 fc_agg_final_costs,
									 fc_dNumGroups));
		}
	}

	/*
	 * 当使用分区聚合时，我们可能在部分路径列表中有完全
	 * 聚合的路径，因为 add_paths_to_append_rel() 会考虑
	 * 由每个子级的非部分路径的并行追加组成的路径用于 grouped_rel。
	 */
	if (fc_grouped_rel->partial_pathlist != NIL)
		fc_gather_grouping_paths(fc_root, fc_grouped_rel);
}

/*
 * create_partial_grouping_paths
 *
 * 创建一个新的上部关系，表示部分聚合的结果
 * 并用适当的路径填充它。注意，我们在这里不对路径列表进行最终化，
 * 所以调用者可以添加其他部分或非部分路径，并且必须随后
 * 调用 gather_grouping_paths 和 set_cheapest 在返回的上部关系上。
 *
 * 此新上部关系的所有路径——部分和非部分——都已经部分聚合
 * 但需要后续的 FinalizeAggregate 步骤。
 *
 * 注意：如果该函数确定没有真正需要创建新的 RelOptInfo，
 * 允许返回 NULL。
 */
static RelOptInfo * fc_create_partial_grouping_paths(PlannerInfo *fc_root,
							  RelOptInfo *fc_grouped_rel,
							  RelOptInfo *fc_input_rel,
							  grouping_sets_data *fc_gd,
							  GroupPathExtraData *fc_extra,
							  bool fc_force_rel_creation)
{
	Query	   *fc_parse = fc_root->parse;
	RelOptInfo *fc_partially_grouped_rel;
	AggClauseCosts *fc_agg_partial_costs = &fc_extra->agg_partial_costs;
	AggClauseCosts *fc_agg_final_costs = &fc_extra->agg_final_costs;
	Path	   *fc_cheapest_partial_path = NULL;
	Path	   *fc_cheapest_total_path = NULL;
	double		fc_dNumPartialGroups = 0;
	double		fc_dNumPartialPartialGroups = 0;
	ListCell   *fc_lc;
	bool		fc_can_hash = (fc_extra->flags & GROUPING_CAN_USE_HASH) != 0;
	bool		fc_can_sort = (fc_extra->flags & GROUPING_CAN_USE_SORT) != 0;

	/*
	 * 考虑我们是否应该生成部分聚合的非部分路径。只有在输入关系的父节点正在执行部分分区聚合时，我们才能这样做，并且前提是我们有一个非部分路径。（注意，extra->patype 是在父级使用的分区聚合类型，而不是这个级别。）
	 */
	if (fc_input_rel->pathlist != NIL &&
		fc_extra->patype == PARTITIONWISE_AGGREGATE_PARTIAL)
		fc_cheapest_total_path = fc_input_rel->cheapest_total_path;

	/*
	 * 如果 grouped_rel 可能并行处理，那么我们应该考虑生成部分分组的部分路径。然而，如果输入关系没有部分路径，我们就无法做到这一点。
	 */
	if (fc_grouped_rel->consider_parallel && fc_input_rel->partial_pathlist != NIL)
		fc_cheapest_partial_path = linitial(fc_input_rel->partial_pathlist);

	/*
	 * 如果我们无法对部分路径进行部分聚合，也无法对非部分路径进行部分聚合，那么就不要创建新的 RelOptInfo，除非调用者指定了 force_rel_creation。
	 */
	if (fc_cheapest_total_path == NULL &&
		fc_cheapest_partial_path == NULL &&
		!fc_force_rel_creation)
		return NULL;

	/*
	 * 构建一个新的上层关系，以表示从输入关系中部分聚合行的结果。
	 */
	fc_partially_grouped_rel = fetch_upper_rel(fc_root,
											UPPERREL_PARTIAL_GROUP_AGG,
											fc_grouped_rel->relids);
	fc_partially_grouped_rel->consider_parallel =
		fc_grouped_rel->consider_parallel;
	fc_partially_grouped_rel->reloptkind = fc_grouped_rel->reloptkind;
	fc_partially_grouped_rel->serverid = fc_grouped_rel->serverid;
	fc_partially_grouped_rel->userid = fc_grouped_rel->userid;
	fc_partially_grouped_rel->useridiscurrent = fc_grouped_rel->useridiscurrent;
	fc_partially_grouped_rel->fdwroutine = fc_grouped_rel->fdwroutine;

	/*
	 * 为部分聚合路径构建目标列表。这些路径不能仅仅发出与常规聚合路径相同的 tlist，因为（1）我们必须包含在 HAVING 中需要的 Vars 和 Aggrefs，这些可能不会出现在结果 tlist 中，和（2）Aggrefs 必须设置为部分模式。
	 */
	fc_partially_grouped_rel->reltarget =
		fc_make_partial_grouping_target(fc_root, fc_grouped_rel->reltarget,
									 fc_extra->havingQual);

	if (!fc_extra->partial_costs_set)
	{
		/*
		 * 收集有关聚合的统计信息，以估算在并行执行聚合时的成本。
		 */
		MemSet(fc_agg_partial_costs, 0, sizeof(AggClauseCosts));
		MemSet(fc_agg_final_costs, 0, sizeof(AggClauseCosts));
		if (fc_parse->hasAggs)
		{
			/* 部分阶段 */
			get_agg_clause_costs(fc_root, AGGSPLIT_INITIAL_SERIAL,
								 fc_agg_partial_costs);

			/* 最终阶段 */
			get_agg_clause_costs(fc_root, AGGSPLIT_FINAL_DESERIAL,
								 fc_agg_final_costs);
		}

		fc_extra->partial_costs_set = true;
	}

	/* 估算部分组的数量。 */
	if (fc_cheapest_total_path != NULL)
		fc_dNumPartialGroups =
			get_number_of_groups(fc_root,
								 fc_cheapest_total_path->rows,
								 fc_gd,
								 fc_extra->targetList);
	if (fc_cheapest_partial_path != NULL)
		fc_dNumPartialPartialGroups =
			get_number_of_groups(fc_root,
								 fc_cheapest_partial_path->rows,
								 fc_gd,
								 fc_extra->targetList);

	if (fc_can_sort && fc_cheapest_total_path != NULL)
	{
		/* 这应该在之前检查过 */
		Assert(fc_parse->hasAggs || fc_parse->groupClause);

		/*
		 * 使用任何可用的适当排序路径作为输入，并考虑对最便宜的部分路径进行排序。
		 */
		foreach(fc_lc, fc_input_rel->pathlist)
		{
			Path	   *fc_path = (Path *) lfirst(fc_lc);
			bool		fc_is_sorted;

			fc_is_sorted = pathkeys_contained_in(fc_root->group_pathkeys,
											  fc_path->pathkeys);
			if (fc_path == fc_cheapest_total_path || fc_is_sorted)
			{
				/* 如果最便宜的部分路径还没有排序，则对其进行排序 */
				if (!fc_is_sorted)
					fc_path = (Path *) create_sort_path(fc_root,
													 fc_partially_grouped_rel,
													 fc_path,
													 fc_root->group_pathkeys,
													 -1.0);

				if (fc_parse->hasAggs)
					add_path(fc_partially_grouped_rel, (Path *)
							 create_agg_path(fc_root,
											 fc_partially_grouped_rel,
											 fc_path,
											 fc_partially_grouped_rel->reltarget,
											 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
											 AGGSPLIT_INITIAL_SERIAL,
											 fc_parse->groupClause,
											 NIL,
											 fc_agg_partial_costs,
											 fc_dNumPartialGroups));
				else
					add_path(fc_partially_grouped_rel, (Path *)
							 create_group_path(fc_root,
											   fc_partially_grouped_rel,
											   fc_path,
											   fc_parse->groupClause,
											   NIL,
											   fc_dNumPartialGroups));
			}
		}

		/*
		 * 如果启用，则考虑对所有部分路径进行增量排序。
		 *
		 * 当我们只有一个单项 group_pathkeys 时，我们也可以跳过整个循环，因为在没有完全排序的列表的情况下，我们无法有一个预排序的列表前缀。
		 */
		if (enable_incremental_sort && list_length(fc_root->group_pathkeys) > 1)
		{
			foreach(fc_lc, fc_input_rel->pathlist)
			{
				Path	   *fc_path = (Path *) lfirst(fc_lc);
				bool		fc_is_sorted;
				int			fc_presorted_keys;

				fc_is_sorted = pathkeys_count_contained_in(fc_root->group_pathkeys,
														fc_path->pathkeys,
														&fc_presorted_keys);

				/* 忽略已经排序的路径 */
				if (fc_is_sorted)
					continue;

				if (fc_presorted_keys == 0)
					continue;

				/* 由于我们有预排序的键，考虑增量排序。 */
				fc_path = (Path *) create_incremental_sort_path(fc_root,
															 fc_partially_grouped_rel,
															 fc_path,
															 fc_root->group_pathkeys,
															 fc_presorted_keys,
															 -1.0);

				if (fc_parse->hasAggs)
					add_path(fc_partially_grouped_rel, (Path *)
							 create_agg_path(fc_root,
											 fc_partially_grouped_rel,
											 fc_path,
											 fc_partially_grouped_rel->reltarget,
											 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
											 AGGSPLIT_INITIAL_SERIAL,
											 fc_parse->groupClause,
											 NIL,
											 fc_agg_partial_costs,
											 fc_dNumPartialGroups));
				else
					add_path(fc_partially_grouped_rel, (Path *)
							 create_group_path(fc_root,
											   fc_partially_grouped_rel,
											   fc_path,
											   fc_parse->groupClause,
											   NIL,
											   fc_dNumPartialGroups));
			}
		}
	}

	if (fc_can_sort && fc_cheapest_partial_path != NULL)
	{
		/* 类似于上述逻辑，但针对部分路径。 */
		foreach(fc_lc, fc_input_rel->partial_pathlist)
		{
			Path	   *fc_path = (Path *) lfirst(fc_lc);
			Path	   *fc_path_original = fc_path;
			bool		fc_is_sorted;
			int			fc_presorted_keys;

			fc_is_sorted = pathkeys_count_contained_in(fc_root->group_pathkeys,
													fc_path->pathkeys,
													&fc_presorted_keys);

			if (fc_path == fc_cheapest_partial_path || fc_is_sorted)
			{
				/* 如果最便宜的部分路径还没有排序，则对其进行排序 */
				if (!fc_is_sorted)
					fc_path = (Path *) create_sort_path(fc_root,
													 fc_partially_grouped_rel,
													 fc_path,
													 fc_root->group_pathkeys,
													 -1.0);

				if (fc_parse->hasAggs)
					add_partial_path(fc_partially_grouped_rel, (Path *)
									 create_agg_path(fc_root,
													 fc_partially_grouped_rel,
													 fc_path,
													 fc_partially_grouped_rel->reltarget,
													 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
													 AGGSPLIT_INITIAL_SERIAL,
													 fc_parse->groupClause,
													 NIL,
													 fc_agg_partial_costs,
													 fc_dNumPartialPartialGroups));
				else
					add_partial_path(fc_partially_grouped_rel, (Path *)
									 create_group_path(fc_root,
													   fc_partially_grouped_rel,
													   fc_path,
													   fc_parse->groupClause,
													   NIL,
													   fc_dNumPartialPartialGroups));
			}

			/*
			 * 现在我们可以考虑在这个路径上进行增量排序，但仅在
			 * 路径尚未排序且启用了增量排序时。
			 */
			if (fc_is_sorted || !enable_incremental_sort)
				continue;

			/* 恢复输入路径（我们可能在顶部添加了 Sort）。 */
			fc_path = fc_path_original;

			/* 无共享前缀，构建增量排序没有意义 */
			if (fc_presorted_keys == 0)
				continue;

			/*
			 * 我们应该已经排除了长度为 1 的 pathkeys，因为
			 * 然后 presorted_keys > 0 将意味着 is_sorted 为 true。
			 */
			Assert(list_length(fc_root->group_pathkeys) != 1);

			fc_path = (Path *) create_incremental_sort_path(fc_root,
														 fc_partially_grouped_rel,
														 fc_path,
														 fc_root->group_pathkeys,
														 fc_presorted_keys,
														 -1.0);

			if (fc_parse->hasAggs)
				add_partial_path(fc_partially_grouped_rel, (Path *)
								 create_agg_path(fc_root,
												 fc_partially_grouped_rel,
												 fc_path,
												 fc_partially_grouped_rel->reltarget,
												 fc_parse->groupClause ? AGG_SORTED : AGG_PLAIN,
												 AGGSPLIT_INITIAL_SERIAL,
												 fc_parse->groupClause,
												 NIL,
												 fc_agg_partial_costs,
												 fc_dNumPartialPartialGroups));
			else
				add_partial_path(fc_partially_grouped_rel, (Path *)
								 create_group_path(fc_root,
												   fc_partially_grouped_rel,
												   fc_path,
												   fc_parse->groupClause,
												   NIL,
												   fc_dNumPartialPartialGroups));
		}
	}

	/*
	 * 在可能的地方添加一个部分分组的 HashAgg 路径
	 */
	if (fc_can_hash && fc_cheapest_total_path != NULL)
	{
		/* 在上面检查过 */
		Assert(fc_parse->hasAggs || fc_parse->groupClause);

		add_path(fc_partially_grouped_rel, (Path *)
				 create_agg_path(fc_root,
								 fc_partially_grouped_rel,
								 fc_cheapest_total_path,
								 fc_partially_grouped_rel->reltarget,
								 AGG_HASHED,
								 AGGSPLIT_INITIAL_SERIAL,
								 fc_parse->groupClause,
								 NIL,
								 fc_agg_partial_costs,
								 fc_dNumPartialGroups));
	}

	/*
	 * 现在在可能的地方添加一个部分分组的 HashAgg 部分路径
	 */
	if (fc_can_hash && fc_cheapest_partial_path != NULL)
	{
		add_partial_path(fc_partially_grouped_rel, (Path *)
						 create_agg_path(fc_root,
										 fc_partially_grouped_rel,
										 fc_cheapest_partial_path,
										 fc_partially_grouped_rel->reltarget,
										 AGG_HASHED,
										 AGGSPLIT_INITIAL_SERIAL,
										 fc_parse->groupClause,
										 NIL,
										 fc_agg_partial_costs,
										 fc_dNumPartialPartialGroups));
	}

	/*
	 * 如果存在一个 FDW 负责查询的所有 baserels，那么就让它考虑添加部分分组的 ForeignPaths。
	 */
	if (fc_partially_grouped_rel->fdwroutine &&
		fc_partially_grouped_rel->fdwroutine->GetForeignUpperPaths)
	{
		FdwRoutine *fc_fdwroutine = fc_partially_grouped_rel->fdwroutine;

		fc_fdwroutine->GetForeignUpperPaths(fc_root,
										 UPPERREL_PARTIAL_GROUP_AGG,
										 fc_input_rel, fc_partially_grouped_rel,
										 fc_extra);
	}

	return fc_partially_grouped_rel;
}

/*
 * 为分组关系或部分分组关系生成 Gather 和 Gather Merge 路径。
 *
 * generate_useful_gather_paths 执行大部分工作，但我们也考虑一个特殊情况：我们可以尝试按 group_pathkeys 对数据进行排序，然后应用 Gather Merge。
 *
 * 注意：此函数不应用于分组或部分分组关系之外的任何内容，不仅因为它明确引用了 group_pathkeys，还因为我们将 "true" 作为第三个参数传递给 generate_useful_gather_paths()。
 */
static void fc_gather_grouping_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	ListCell   *fc_lc;
	Path	   *fc_cheapest_partial_path;

	/* 对无序路径尝试 Gather，对有序路径尝试 Gather Merge。 */
	generate_useful_gather_paths(fc_root, fc_rel, true);

	/* 尝试最便宜的部分路径 + 显式排序 + Gather Merge。 */
	fc_cheapest_partial_path = linitial(fc_rel->partial_pathlist);
	if (!pathkeys_contained_in(fc_root->group_pathkeys,
							   fc_cheapest_partial_path->pathkeys))
	{
		Path	   *fc_path;
		double		fc_total_groups;

		fc_total_groups =
			fc_cheapest_partial_path->rows * fc_cheapest_partial_path->parallel_workers;
		fc_path = (Path *) create_sort_path(fc_root, fc_rel, fc_cheapest_partial_path,
										 fc_root->group_pathkeys,
										 -1.0);
		fc_path = (Path *)
			create_gather_merge_path(fc_root,
									 fc_rel,
									 fc_path,
									 fc_rel->reltarget,
									 fc_root->group_pathkeys,
									 NULL,
									 &fc_total_groups);

		add_path(fc_rel, fc_path);
	}

	/*
	 * 如果启用，则考虑对所有部分路径进行增量排序。
	 *
	 * 当我们只有一个单项 group_pathkeys 时，我们也可以跳过整个循环，因为在没有完全排序的列表的情况下，我们无法有一个预排序的列表前缀。
	 */
	if (!enable_incremental_sort || list_length(fc_root->group_pathkeys) == 1)
		return;

	/* 如果启用，也考虑对部分路径进行增量排序 */
	foreach(fc_lc, fc_rel->partial_pathlist)
	{
		Path	   *fc_path = (Path *) lfirst(fc_lc);
		bool		fc_is_sorted;
		int			fc_presorted_keys;
		double		fc_total_groups;

		fc_is_sorted = pathkeys_count_contained_in(fc_root->group_pathkeys,
												fc_path->pathkeys,
												&fc_presorted_keys);

		if (fc_is_sorted)
			continue;

		if (fc_presorted_keys == 0)
			continue;

		fc_path = (Path *) create_incremental_sort_path(fc_root,
													 fc_rel,
													 fc_path,
													 fc_root->group_pathkeys,
													 fc_presorted_keys,
													 -1.0);

		fc_path = (Path *)
			create_gather_merge_path(fc_root,
									 fc_rel,
									 fc_path,
									 fc_rel->reltarget,
									 fc_root->group_pathkeys,
									 NULL,
									 &fc_total_groups);

		add_path(fc_rel, fc_path);
	}
}

/*
 * can_partial_agg
 *
 * 确定部分分组和/或聚合是否可能。可行时返回 true，否则返回 false。
 */
static bool fc_can_partial_agg(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;

	if (!fc_parse->hasAggs && fc_parse->groupClause == NIL)
	{
		/*
		 * 除非我们有某些聚合或分组子句，否则我们不知道如何进行并行聚合。
		 */
		return false;
	}
	else if (fc_parse->groupingSets)
	{
		/* 我们不知道如何进行并行分组集。 */
		return false;
	}
	else if (fc_root->hasNonPartialAggs || fc_root->hasNonSerialAggs)
	{
		/* 部分模式支持不足。 */
		return false;
	}

	/* 一切看起来都很好。 */
	return true;
}

/*
 * apply_scanjoin_target_to_paths
 *
 * 调整最终的扫描/连接关系，以及其所有子关系，以生成最终的扫描/连接目标。将其建模为一个单独的规划步骤，并在顶层和每个子关系上创建一个新的 RelOptInfo 更为正确，但这样做明显更便宜。也许那个问题可以在某个时刻解决，但现在我们这样做。
 *
 * 如果 tlist_same_exprs 为真，则将要应用的扫描/连接目标与现有的 reltarget 具有相同的表达式，因此我们只需插入适当的 sortgroupref 信息。通过避免创建投影路径，我们在立即和计划创建时都节省了工作量。
 */
static void fc_apply_scanjoin_target_to_paths(PlannerInfo *fc_root,
							   RelOptInfo *fc_rel,
							   List *fc_scanjoin_targets,
							   List *fc_scanjoin_targets_contain_srfs,
							   bool fc_scanjoin_target_parallel_safe,
							   bool fc_tlist_same_exprs)
{
	bool		fc_rel_is_partitioned = IS_PARTITIONED_REL(fc_rel);
	PathTarget *fc_scanjoin_target;
	ListCell   *fc_lc;

	/* 这会递归，因此要小心。 */
	check_stack_depth();

	/*
	 * 如果关系是分区的，我们希望删除其现有路径并生成新的路径。如果我们保留现有路径，这个函数仍然是正确的：我们会修改它们以生成位于分区 Append 之上的正确目标，然后它们将与生成位于 Append 之下的目标的路径竞争成本。然而，在我们当前的成本模型中，后者的成本总是相同或更低，因此修改现有路径只是无用的工作。此外，当成本相同时，变动的舍入误差有时可能允许选择现有路径，导致不希望的跨平台计划变化。因此，我们丢弃旧路径，从而强制工作在 Append 之下进行，除非是非并行安全目标的情况。
	 *
	 * 需要小心，因为我们必须允许 generate_useful_gather_paths 在下一部分看到旧的部分路径。因此，在这里清除主路径列表，然后允许 generate_useful_gather_paths 将路径添加到主列表，最后清除部分路径列表。
	 */
	if (fc_rel_is_partitioned)
		fc_rel->pathlist = NIL;

	/*
	 * 如果扫描/连接目标不是并行安全的，则部分路径无法生成它。
	 */
	if (!fc_scanjoin_target_parallel_safe)
	{
		/*
		 * 由于我们无法在并行工作进程中生成最终的扫描/连接目标，这是我们最后一次利用任何存在的部分路径的机会；因此构建使用它们的 Gather 路径，并发出当前的 reltarget。对于目标是并行安全的情况，我们不这样做，因为在最终扫描/连接目标被应用之后，我们将能够生成优越的路径。
		 */
		generate_useful_gather_paths(fc_root, fc_rel, false);

		/* 不能在此级别以上使用并行查询。 */
		fc_rel->partial_pathlist = NIL;
		fc_rel->consider_parallel = false;
	}

	/* 完成删除分区关系的旧路径，参见上面的注释 */
	if (fc_rel_is_partitioned)
		fc_rel->partial_pathlist = NIL;

	/* 提取不包含 SRF 的扫描/连接目标。 */
	fc_scanjoin_target = linitial_node(PathTarget, fc_scanjoin_targets);

	/*
	 * 将不包含 SRF 的扫描/连接目标应用到每个现有路径。
	 *
	 * 如果 tlist 表达式相同，我们可以仅将 sortgroupref 信息注入现有的 pathtargets 中。否则，用生成不包含 SRF 的扫描/连接目标的投影路径替换每个路径。这不能改变 rel->pathlist 中路径的顺序，因此我们在原地修改列表。
	 */
	foreach(fc_lc, fc_rel->pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);

		/* 不应该再有任何参数化路径 */
		Assert(fc_subpath->param_info == NULL);

		if (fc_tlist_same_exprs)
			fc_subpath->pathtarget->sortgrouprefs =
				fc_scanjoin_target->sortgrouprefs;
		else
		{
			Path	   *fc_newpath;

			fc_newpath = (Path *) create_projection_path(fc_root, fc_rel, fc_subpath,
													  fc_scanjoin_target);
			lfirst(fc_lc) = fc_newpath;
		}
	}

	/* 同样调整任何部分路径的目标。 */
	foreach(fc_lc, fc_rel->partial_pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);

		/* 不应该再有任何参数化路径 */
		Assert(fc_subpath->param_info == NULL);

		if (fc_tlist_same_exprs)
			fc_subpath->pathtarget->sortgrouprefs =
				fc_scanjoin_target->sortgrouprefs;
		else
		{
			Path	   *fc_newpath;

			fc_newpath = (Path *) create_projection_path(fc_root, fc_rel, fc_subpath,
													  fc_scanjoin_target);
			lfirst(fc_lc) = fc_newpath;
		}
	}

	/*
	 * 现在，如果最终扫描/连接目标包含 SRFs，则在每个现有路径顶部插入 ProjectSetPath(s)。 （注意，此函数不查看 cheapest-path 字段，这是一件好事，因为它们现在是虚假的。）
	 */
	if (fc_root->parse->hasTargetSRFs)
		fc_adjust_paths_for_srfs(fc_root, fc_rel,
							  fc_scanjoin_targets,
							  fc_scanjoin_targets_contain_srfs);

	/*
	 * 更新关系的目标，使其成为最终（包含 SRFs）的扫描/连接目标。这现在与所有路径的实际输出相匹配，如果它们不一致，我们可能会在 createplan.c 中感到困惑。我们必须现在这样做，以便在下一部分所做的任何附加路径将使用正确的 pathtarget（参见 create_append_path）。
	 *
	 * 请注意，如果在最终扫描/连接关系或其任何子关系上调用 GetForeignUpperPaths()，这也是必要的，因为 FDW 可能会查看关系的目标以创建 ForeignPaths。
	 */
	fc_rel->reltarget = llast_node(PathTarget, fc_scanjoin_targets);

	/*
	 * 如果关系是分区的，递归地将扫描/连接目标应用到所有分区，并生成全新的 Append 路径，其中扫描/连接目标是在 Append 之下而不是在其上计算的。由于 Append 不是投影能力的，这可能会节省一个单独的结果节点，并且对于分区聚合也很重要。
	 */
	if (fc_rel_is_partitioned)
	{
		List	   *fc_live_children = NIL;
		int			fc_i;

		/* 调整每个分区。 */
		fc_i = -1;
		while ((fc_i = bms_next_member(fc_rel->live_parts, fc_i)) >= 0)
		{
			RelOptInfo *fc_child_rel = fc_rel->part_rels[fc_i];
			AppendRelInfo **fc_appinfos;
			int			fc_nappinfos;
			List	   *fc_child_scanjoin_targets = NIL;
			ListCell   *fc_lc;

			Assert(fc_child_rel != NULL);

			/* 可以忽略虚拟子项。 */
			if (IS_DUMMY_REL(fc_child_rel))
				continue;

			/* 翻译此子项的扫描/连接目标。 */
			fc_appinfos = find_appinfos_by_relids(fc_root, fc_child_rel->relids,
											   &fc_nappinfos);
			foreach(fc_lc, fc_scanjoin_targets)
			{
				PathTarget *fc_target = lfirst_node(PathTarget, fc_lc);

				fc_target = copy_pathtarget(fc_target);
				fc_target->exprs = (List *)
					adjust_appendrel_attrs(fc_root,
										   (Node *) fc_target->exprs,
										   fc_nappinfos, fc_appinfos);
				fc_child_scanjoin_targets = lappend(fc_child_scanjoin_targets,
												 fc_target);
			}
			pfree(fc_appinfos);

			/* 递归完成实际工作。 */
			fc_apply_scanjoin_target_to_paths(fc_root, fc_child_rel,
										   fc_child_scanjoin_targets,
										   fc_scanjoin_targets_contain_srfs,
										   fc_scanjoin_target_parallel_safe,
										   fc_tlist_same_exprs);

			/* 为 Append 路径保存非虚拟子项。 */
			if (!IS_DUMMY_REL(fc_child_rel))
				fc_live_children = lappend(fc_live_children, fc_child_rel);
		}

		/* 通过附加子路径为此关系构建新路径。 */
		add_paths_to_append_rel(fc_root, fc_rel, fc_live_children);
	}

	
/*
	 * 考虑生成 Gather 或 Gather Merge 路径。我们仅在关系是并行安全的情况下执行此操作，并且不对子关系执行此操作，以避免在同一计划中创建多个 Gather 节点。在生成所有路径之后且在 set_cheapest 之前，我们必须执行此操作，因为生成的路径之一可能会成为最便宜的路径。
	 */
	if (fc_rel->consider_parallel && !IS_OTHER_REL(fc_rel))
		generate_useful_gather_paths(fc_root, fc_rel, false);

	/*
	 * 重新评估哪些路径是最便宜的，现在我们可能已将新的 Gather（或 Gather Merge）和/或 Append（或 MergeAppend）路径添加到该关系。
	 */
	set_cheapest(fc_rel);
}

/*
 * create_partitionwise_grouping_paths
 *
 * 如果输入关系的分区键是 GROUP BY 子句的一部分，则所有属于给定组的行都来自单个分区。这允许对分区关系的聚合/分组进行分解，聚合/分组到每个分区。这应该不比正常方法差，通常会更好。
 *
 * 然而，如果 GROUP BY 子句不包含所有分区键，则来自给定组的行可能分散在多个分区。在这种情况下，我们对每个组执行部分聚合，附加结果，然后最终化聚合。这在获胜的确定性上不如前一种情况。如果 PartialAggregate 阶段大大减少了组的数量，那么可能会获胜，因为通过 Append 节点的行数较少。如果我们有很多小组，则可能会失去。
 */
static void fc_create_partitionwise_grouping_paths(PlannerInfo *fc_root,
									RelOptInfo *fc_input_rel,
									RelOptInfo *fc_grouped_rel,
									RelOptInfo *fc_partially_grouped_rel,
									const AggClauseCosts *fc_agg_costs,
									grouping_sets_data *fc_gd,
									PartitionwiseAggregateType fc_patype,
									GroupPathExtraData *fc_extra)
{
	List	   *fc_grouped_live_children = NIL;
	List	   *fc_partially_grouped_live_children = NIL;
	PathTarget *fc_target = fc_grouped_rel->reltarget;
	bool		fc_partial_grouping_valid = true;
	int			fc_i;

	Assert(fc_patype != PARTITIONWISE_AGGREGATE_NONE);
	Assert(fc_patype != PARTITIONWISE_AGGREGATE_PARTIAL ||
		   fc_partially_grouped_rel != NULL);

	/* 为分区聚合/分组添加路径。 */
	fc_i = -1;
	while ((fc_i = bms_next_member(fc_input_rel->live_parts, fc_i)) >= 0)
	{
		RelOptInfo *fc_child_input_rel = fc_input_rel->part_rels[fc_i];
		PathTarget *fc_child_target;
		AppendRelInfo **fc_appinfos;
		int			fc_nappinfos;
		GroupPathExtraData fc_child_extra;
		RelOptInfo *fc_child_grouped_rel;
		RelOptInfo *fc_child_partially_grouped_rel;

		Assert(fc_child_input_rel != NULL);

		/* 可以忽略虚拟子项。 */
		if (IS_DUMMY_REL(fc_child_input_rel))
			continue;

		fc_child_target = copy_pathtarget(fc_target);

		/*
		 * 按原样复制给定的“额外”结构，然后覆盖特定于该子项的成员。
		 */
		memcpy(&fc_child_extra, fc_extra, sizeof(fc_child_extra));

		fc_appinfos = find_appinfos_by_relids(fc_root, fc_child_input_rel->relids,
										   &fc_nappinfos);

		fc_child_target->exprs = (List *)
			adjust_appendrel_attrs(fc_root,
								   (Node *) fc_target->exprs,
								   fc_nappinfos, fc_appinfos);

		/* 转换 havingQual 和 targetList。 */
		fc_child_extra.havingQual = (Node *)
			adjust_appendrel_attrs(fc_root,
								   fc_extra->havingQual,
								   fc_nappinfos, fc_appinfos);
		fc_child_extra.targetList = (List *)
			adjust_appendrel_attrs(fc_root,
								   (Node *) fc_extra->targetList,
								   fc_nappinfos, fc_appinfos);

		/*
		 * extra->patype 是为我们父关系计算的值; patype 是该关系的值。对于子项，我们的值是其父关系的值。
		 */
		fc_child_extra.patype = fc_patype;

		/*
		 * 创建分组关系以保存子项的完全聚合分组和/或聚合路径。
		 */
		fc_child_grouped_rel = fc_make_grouping_rel(fc_root, fc_child_input_rel,
											  fc_child_target,
											  fc_extra->target_parallel_safe,
											  fc_child_extra.havingQual);

		/* 为该子关系创建分组路径。 */
		fc_create_ordinary_grouping_paths(fc_root, fc_child_input_rel,
									   fc_child_grouped_rel,
									   fc_agg_costs, fc_gd, &fc_child_extra,
									   &fc_child_partially_grouped_rel);

		if (fc_child_partially_grouped_rel)
		{
			fc_partially_grouped_live_children =
				lappend(fc_partially_grouped_live_children,
						fc_child_partially_grouped_rel);
		}
		else
			fc_partial_grouping_valid = false;

		if (fc_patype == PARTITIONWISE_AGGREGATE_FULL)
		{
			set_cheapest(fc_child_grouped_rel);
			fc_grouped_live_children = lappend(fc_grouped_live_children,
											fc_child_grouped_rel);
		}

		pfree(fc_appinfos);
	}

	/*
	 * 尝试为部分分组的子项创建附加路径。对于完整的分区聚合，如果可能进行并行聚合，我们可能在 partial_pathlist 中有路径。对于部分分区聚合，我们可能在 pathlist 和 partial_pathlist 中都有路径。
	 *
	 * 注意：我们必须为每个子项拥有一个部分分组路径，以便为该关系生成一个部分分组路径。
	 */
	if (fc_partially_grouped_rel && fc_partial_grouping_valid)
	{
		Assert(fc_partially_grouped_live_children != NIL);

		add_paths_to_append_rel(fc_root, fc_partially_grouped_rel,
								fc_partially_grouped_live_children);

		/*
		 * 我们需要调用 set_cheapest，因为最终步骤将使用
		 * rel 的最便宜路径。
		 */
		if (fc_partially_grouped_rel->pathlist)
			set_cheapest(fc_partially_grouped_rel);
	}

	/* 如果可能，为完全分组的子项创建附加路径。 */
	if (fc_patype == PARTITIONWISE_AGGREGATE_FULL)
	{
		Assert(fc_grouped_live_children != NIL);

		add_paths_to_append_rel(fc_root, fc_grouped_rel, fc_grouped_live_children);
	}
}

/*
 * group_by_has_partkey
 *
 * 如果给定关系的所有分区键都是
 * GROUP BY 子句的一部分，包括具有匹配的排序规则，则返回 true，否则返回 false。
 */
static bool fc_group_by_has_partkey(RelOptInfo *fc_input_rel,
					 List *fc_targetList,
					 List *fc_groupClause)
{
	List	   *fc_groupexprs = get_sortgrouplist_exprs(fc_groupClause, fc_targetList);
	int			fc_cnt = 0;
	int			fc_partnatts;

	/* 输入关系应为分区的。 */
	Assert(fc_input_rel->part_scheme);

	/* 如果不存在分区键，则过早排除。 */
	if (!fc_input_rel->partexprs)
		return false;

	fc_partnatts = fc_input_rel->part_scheme->partnatts;

	for (fc_cnt = 0; fc_cnt < fc_partnatts; fc_cnt++)
	{
		List	   *fc_partexprs = fc_input_rel->partexprs[fc_cnt];
		ListCell   *fc_lc;
		bool		fc_found = false;

		foreach(fc_lc, fc_partexprs)
		{
			ListCell   *fc_lg;
			Expr	   *fc_partexpr = lfirst(fc_lc);
			Oid			fc_partcoll = fc_input_rel->part_scheme->partcollation[fc_cnt];

			foreach(fc_lg, fc_groupexprs)
			{
				Expr	   *fc_groupexpr = lfirst(fc_lg);
				Oid			fc_groupcoll = exprCollation((Node *) fc_groupexpr);

				/*
				 * 注意：我们可以假设最多只有一个 RelabelType 节点；
				 * eval_const_expressions() 如果存在多个将进行简化。
				 */
				if (IsA(fc_groupexpr, RelabelType))
					fc_groupexpr = ((RelabelType *) fc_groupexpr)->arg;

				if (equal(fc_groupexpr, fc_partexpr))
				{
					/*
					 * 如果分组的排序规则与
					 * 分区的排序规则不匹配，则拒绝匹配。
					 */
					if (OidIsValid(fc_partcoll) && OidIsValid(fc_groupcoll) &&
						fc_partcoll != fc_groupcoll)
						return false;

					fc_found = true;
					break;
				}
			}

			if (fc_found)
				break;
		}

		/*
		 * 如果没有任何分区键表达式与任何
		 * GROUP BY 表达式匹配，返回 false。
		 */
		if (!fc_found)
			return false;
	}

	return true;
}
